From c779524aad5334d95a6b948b8d816c266da10570 Mon Sep 17 00:00:00 2001
From: Frederik Gartenmeister <mustermeiszer@posteo.de>
Date: Tue, 12 Sep 2023 23:03:12 +0200
Subject: [PATCH] precompile: Remove extra checks (#1538)

* precompile: Remove extra checks

* precompile: Update precompile test

* precompile: Add checks for account balances in integration test

* taplo: Fix

* lp-gateway: Ensure contract address is encoded properly

* precompile: Add support for hex source address

* development: Update spec version

* clippy: Fix

---------

Co-authored-by: cdamian <17934949+cdamian@users.noreply.github.com>
---
 Cargo.lock                                    |   6 +-
 .../axelar-gateway-precompile/Cargo.toml      |   1 +
 .../axelar-gateway-precompile/src/lib.rs      | 212 +++++++---------
 .../routers/Cargo.toml                        |   1 +
 .../routers/src/routers/axelar_evm.rs         |  11 +-
 runtime/common/src/evm/precompile.rs          |   2 +-
 runtime/development/Cargo.toml                |   2 +-
 runtime/development/src/lib.rs                |   2 +-
 runtime/integration-tests/Cargo.toml          |   3 +
 .../pallet.rs => evm/ethereum_transaction.rs} |   1 -
 .../src/{ethereum_transaction => evm}/mod.rs  |   3 +-
 .../integration-tests/src/evm/precompile.rs   | 232 ++++++++++++++++++
 runtime/integration-tests/src/lib.rs          |   2 +-
 13 files changed, 343 insertions(+), 135 deletions(-)
 rename runtime/integration-tests/src/{ethereum_transaction/pallet.rs => evm/ethereum_transaction.rs} (99%)
 rename runtime/integration-tests/src/{ethereum_transaction => evm}/mod.rs (93%)
 create mode 100644 runtime/integration-tests/src/evm/precompile.rs

diff --git a/Cargo.lock b/Cargo.lock
index bb1017d780..0e05e881ab 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -610,6 +610,7 @@ dependencies = [
  "frame-benchmarking",
  "frame-support",
  "frame-system",
+ "hex",
  "pallet-evm",
  "pallet-liquidity-pools-gateway",
  "parity-scale-codec 3.6.4",
@@ -2681,7 +2682,7 @@ dependencies = [
 
 [[package]]
 name = "development-runtime"
-version = "0.10.21"
+version = "0.10.23"
 dependencies = [
  "axelar-gateway-precompile",
  "cfg-primitives",
@@ -5805,6 +5806,7 @@ dependencies = [
  "ethabi 16.0.0",
  "frame-support",
  "frame-system",
+ "hex",
  "lazy_static",
  "orml-traits",
  "pallet-balances",
@@ -11024,6 +11026,7 @@ name = "runtime-integration-tests"
 version = "0.1.0"
 dependencies = [
  "altair-runtime",
+ "axelar-gateway-precompile",
  "centrifuge-runtime",
  "cfg-primitives",
  "cfg-traits",
@@ -11031,6 +11034,7 @@ dependencies = [
  "cfg-utils",
  "cumulus-primitives-core",
  "development-runtime",
+ "ethabi 16.0.0",
  "ethereum",
  "frame-benchmarking",
  "frame-support",
diff --git a/pallets/liquidity-pools-gateway/axelar-gateway-precompile/Cargo.toml b/pallets/liquidity-pools-gateway/axelar-gateway-precompile/Cargo.toml
index 63a59815c0..52dacaed9b 100644
--- a/pallets/liquidity-pools-gateway/axelar-gateway-precompile/Cargo.toml
+++ b/pallets/liquidity-pools-gateway/axelar-gateway-precompile/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+hex = { version = "0.4.3", default-features = false }
 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" }
diff --git a/pallets/liquidity-pools-gateway/axelar-gateway-precompile/src/lib.rs b/pallets/liquidity-pools-gateway/axelar-gateway-precompile/src/lib.rs
index 2a272a754b..7fe1ceea51 100644
--- a/pallets/liquidity-pools-gateway/axelar-gateway-precompile/src/lib.rs
+++ b/pallets/liquidity-pools-gateway/axelar-gateway-precompile/src/lib.rs
@@ -12,20 +12,22 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use cfg_types::domain_address::{Domain, DomainAddress};
-use codec::alloc::string::ToString;
-use ethabi::Token;
 use fp_evm::PrecompileHandle;
-use frame_support::{Blake2_256, StorageHasher};
+use frame_support::ensure;
 use pallet_evm::{ExitError, PrecompileFailure};
 use precompile_utils::prelude::*;
-use sp_core::{bounded::BoundedVec, ConstU32, H160, H256, U256};
-use sp_runtime::{DispatchError, DispatchResult};
+use sp_core::{bounded::BoundedVec, ConstU32, H256, U256};
+use sp_runtime::{
+	traits::{BlakeTwo256, Hash},
+	DispatchError,
+};
 use sp_std::vec::Vec;
 
 pub use crate::weights::WeightInfo;
 
 pub const MAX_SOURCE_CHAIN_BYTES: u32 = 128;
-pub const MAX_SOURCE_ADDRESS_BYTES: u32 = 32;
+// Ensure we allow enough to support a hex encoded address with the `0x` prefix.
+pub const MAX_SOURCE_ADDRESS_BYTES: u32 = 42;
 pub const MAX_TOKEN_SYMBOL_BYTES: u32 = 32;
 pub const MAX_PAYLOAD_BYTES: u32 = 1024;
 pub const PREFIX_CONTRACT_CALL_APPROVED: [u8; 32] = keccak256!("contract-call-approved");
@@ -47,7 +49,7 @@ pub mod weights;
 	frame_support::RuntimeDebugNoBound,
 )]
 pub struct SourceConverter {
-	domain: Domain,
+	pub domain: Domain,
 }
 
 impl SourceConverter {
@@ -116,7 +118,7 @@ pub mod pallet {
 	}
 
 	#[pallet::storage]
-	pub type AxelarGatewayContract<T: Config> = StorageValue<_, H160, ValueQuery>;
+	pub type GatewayContract<T: Config> = StorageValue<_, H160, ValueQuery>;
 
 	/// `SourceConversion` is a `hash_of(Vec<u8>)` where the `Vec<u8>` is the
 	/// blake256-hash of the source-chain identifier used by the Axelar network.
@@ -142,7 +144,7 @@ pub mod pallet {
 	#[pallet::genesis_build]
 	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
 		fn build(&self) {
-			AxelarGatewayContract::<T>::set(self.gateway)
+			GatewayContract::<T>::set(self.gateway)
 		}
 	}
 
@@ -174,7 +176,7 @@ pub mod pallet {
 		pub fn set_gateway(origin: OriginFor<T>, address: H160) -> DispatchResult {
 			<T as Config>::AdminOrigin::ensure_origin(origin)?;
 
-			AxelarGatewayContract::<T>::set(address);
+			GatewayContract::<T>::set(address);
 
 			Self::deposit_event(Event::<T>::GatewaySet { address });
 
@@ -205,9 +207,8 @@ impl<T: Config> cfg_traits::TryConvert<(Vec<u8>, Vec<u8>), DomainAddress> for Pa
 	fn try_convert(origin: (Vec<u8>, Vec<u8>)) -> Result<DomainAddress, DispatchError> {
 		let (source_chain, source_address) = origin;
 
-		let domain_converter =
-			SourceConversion::<T>::get(H256::from(Blake2_256::hash(&source_chain)))
-				.ok_or(Error::<T>::NoConverterForSource)?;
+		let domain_converter = SourceConversion::<T>::get(BlakeTwo256::hash(&source_chain))
+			.ok_or(Error::<T>::NoConverterForSource)?;
 
 		domain_converter
 			.try_convert(&source_address)
@@ -246,30 +247,17 @@ where
 	#[precompile::public("execute(bytes32,string,string,bytes)")]
 	fn execute(
 		handle: &mut impl PrecompileHandle,
-		command_id: H256,
+		_command_id: H256,
 		source_chain: String<MAX_SOURCE_CHAIN_BYTES>,
 		source_address: String<MAX_SOURCE_ADDRESS_BYTES>,
 		payload: Bytes<MAX_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(&ethabi::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())
-			})?),
-			Token::Address(handle.context().address),
-			Token::FixedBytes(payload_hash.as_bytes().into()),
-		])));
+		ensure!(
+			handle.context().caller == GatewayContract::<T>::get(),
+			PrecompileFailure::Error {
+				exit_status: ExitError::Other("gateway contract address mismatch".into()),
+			}
+		);
 
 		let msg = BoundedVec::<
 			u8,
@@ -279,20 +267,34 @@ where
 			exit_status: ExitError::Other("payload conversion".into()),
 		})?;
 
-		Self::execute_call(key, || {
-			let domain_converter =
-				SourceConversion::<T>::get(H256::from(Blake2_256::hash(source_chain.as_bytes())))
-					.ok_or(Error::<T>::NoConverterForSource)?;
+		let domain_converter = SourceConversion::<T>::get(BlakeTwo256::hash(
+			source_chain.as_bytes(),
+		))
+		.ok_or(PrecompileFailure::Error {
+			exit_status: ExitError::Other("converter for source not found".into()),
+		})?;
 
-			let domain_address = domain_converter
-				.try_convert(source_address.as_bytes())
-				.ok_or(Error::<T>::AccountBytesMismatchForDomain)?;
+		let source_address_bytes =
+			get_source_address_bytes(source_address).ok_or(PrecompileFailure::Error {
+				exit_status: ExitError::Other("invalid source address".into()),
+			})?;
 
-			pallet_liquidity_pools_gateway::Pallet::<T>::process_msg(
-				pallet_liquidity_pools_gateway::GatewayOrigin::Domain(domain_address).into(),
-				msg,
-			)
-		})
+		let domain_address = domain_converter
+			.try_convert(source_address_bytes.as_slice())
+			.ok_or(PrecompileFailure::Error {
+				exit_status: ExitError::Other("account bytes mismatch for domain".into()),
+			})?;
+
+		match pallet_liquidity_pools_gateway::Pallet::<T>::process_msg(
+			pallet_liquidity_pools_gateway::GatewayOrigin::Domain(domain_address).into(),
+			msg,
+		)
+		.map(|_| ())
+		.map_err(TryDispatchError::Substrate)
+		{
+			Err(e) => Err(e.into()),
+			Ok(()) => Ok(()),
+		}
 	}
 
 	// Mimics:
@@ -323,98 +325,56 @@ where
 		// TODO: Check whether this is enough or if we should error out
 		Ok(())
 	}
+}
 
-	fn execute_call(key: H256, f: impl FnOnce() -> DispatchResult) -> EvmResult {
-		let gateway = AxelarGatewayContract::<T>::get();
+const EXPECTED_SOURCE_ADDRESS_SIZE: usize = 20;
+const HEX_PREFIX: &str = "0x";
 
-		let valid = Self::get_validate_call(gateway, key);
+pub(crate) fn get_source_address_bytes(
+	source_address: String<MAX_SOURCE_ADDRESS_BYTES>,
+) -> Option<Vec<u8>> {
+	if source_address.as_bytes().len() == EXPECTED_SOURCE_ADDRESS_SIZE {
+		return Some(source_address.as_bytes().to_vec());
+	}
 
-		if valid {
-			// Prevent re-entrance
-			Self::set_validate_call(gateway, key, false);
+	let str = source_address.as_str().ok()?;
 
-			match f().map(|_| ()).map_err(TryDispatchError::Substrate) {
-				Err(e) => {
-					Self::set_validate_call(gateway, key, true);
-					Err(e.into())
-				}
-				Ok(()) => Ok(()),
-			}
-		} else {
-			Err(RevertReason::Custom("Call not validated".to_string()).into())
+	// Attempt to hex decode source address.
+	match hex::decode(str) {
+		Ok(res) => Some(res),
+		Err(_) => {
+			// Strip 0x prefix.
+			let res = str.strip_prefix(HEX_PREFIX)?;
+
+			hex::decode(res).ok()
 		}
 	}
+}
 
-	fn get_validate_call(from: H160, key: H256) -> bool {
-		Self::h256_to_bool(pallet_evm::AccountStorages::<T>::get(
-			from,
-			Self::get_index_validate_call(key),
-		))
-	}
+#[cfg(test)]
+mod tests {
+	use sp_core::H160;
 
-	fn set_validate_call(from: H160, key: H256, valid: bool) {
-		pallet_evm::AccountStorages::<T>::set(
-			from,
-			Self::get_index_validate_call(key),
-			Self::bool_to_h256(valid),
-		)
-	}
+	use super::*;
 
-	fn get_index_validate_call(key: H256) -> H256 {
-		// Generate right index:
-		//
-		// From the solidty contract of Axelar (EternalStorage.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))
-	}
+	#[test]
+	fn get_source_address_bytes_works() {
+		let hash = H160::from_low_u64_be(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 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
-	}
+		let str = String::<MAX_SOURCE_ADDRESS_BYTES>::from(hash.as_fixed_bytes().to_vec());
 
-	// 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;
-		}
+		get_source_address_bytes(str).expect("address bytes from H160 works");
+
+		let str = String::<MAX_SOURCE_ADDRESS_BYTES>::from(
+			"d47ed02acbbb66ee8a3fe0275bd98add0aa607c3".to_string(),
+		);
+
+		get_source_address_bytes(str).expect("address bytes from un-prefixed hex works");
+
+		let str = String::<MAX_SOURCE_ADDRESS_BYTES>::from(
+			"0xd47ed02acbbb66ee8a3fe0275bd98add0aa607c3".to_string(),
+		);
 
-		H256::from(bytes)
+		get_source_address_bytes(str).expect("address bytes from prefixed hex works");
 	}
 }
diff --git a/pallets/liquidity-pools-gateway/routers/Cargo.toml b/pallets/liquidity-pools-gateway/routers/Cargo.toml
index d0ba8b5395..fae92ee6ea 100644
--- a/pallets/liquidity-pools-gateway/routers/Cargo.toml
+++ b/pallets/liquidity-pools-gateway/routers/Cargo.toml
@@ -11,6 +11,7 @@ version = "0.0.1"
 targets = ["x86_64-unknown-linux-gnu"]
 
 [dependencies]
+hex = { version = "0.4.3", default-features = false }
 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" }
diff --git a/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs b/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs
index 137160536f..a5912166fd 100644
--- a/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs
+++ b/pallets/liquidity-pools-gateway/routers/src/routers/axelar_evm.rs
@@ -15,7 +15,10 @@ use ethabi::{Contract, Function, Param, ParamType, Token};
 use frame_support::dispatch::{DispatchError, DispatchResult};
 use frame_system::pallet_prelude::OriginFor;
 use scale_info::{
-	prelude::string::{String, ToString},
+	prelude::{
+		format,
+		string::{String, ToString},
+	},
 	TypeInfo,
 };
 use sp_core::{bounded::BoundedVec, ConstU32, H160};
@@ -155,7 +158,11 @@ pub(crate) fn get_axelar_encoded_msg(
 	.map_err(|_| "cannot retrieve Axelar contract function")?
 	.encode_input(&[
 		Token::String(target_chain_string),
-		Token::String(target_contract.to_string()),
+		// Ensure that the target contract is correctly converted to hex.
+		//
+		// The `to_string` method on the H160 is returning a string containing an ellipsis, such
+		// as: 0x1234…7890
+		Token::String(format!("0x{}", hex::encode(target_contract.0))),
 		Token::Bytes(encoded_liquidity_pools_contract),
 	])
 	.map_err(|_| "cannot encode input for Axelar contract function")?;
diff --git a/runtime/common/src/evm/precompile.rs b/runtime/common/src/evm/precompile.rs
index cf0987f909..c204976fb6 100644
--- a/runtime/common/src/evm/precompile.rs
+++ b/runtime/common/src/evm/precompile.rs
@@ -46,7 +46,7 @@ const ECRECOVERPUBLICKEY_ADDR: Addr = addr(1026);
 /// Liquidity-Pool logic on centrifuge.
 ///
 /// The precompile implements
-const LP_AXELAR_GATEWAY: Addr = addr(2048);
+pub const LP_AXELAR_GATEWAY: Addr = addr(2048);
 
 pub struct CentrifugePrecompiles<R>(PhantomData<R>);
 
diff --git a/runtime/development/Cargo.toml b/runtime/development/Cargo.toml
index 111f6227b9..775a798b35 100644
--- a/runtime/development/Cargo.toml
+++ b/runtime/development/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "development-runtime"
-version = "0.10.21"
+version = "0.10.23"
 authors = ["Centrifuge <admin@centrifuge.io>"]
 edition = "2021"
 build = "build.rs"
diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs
index 191e9a6147..145a8dfe3f 100644
--- a/runtime/development/src/lib.rs
+++ b/runtime/development/src/lib.rs
@@ -138,7 +138,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: create_runtime_str!("centrifuge-devel"),
 	impl_name: create_runtime_str!("centrifuge-devel"),
 	authoring_version: 1,
-	spec_version: 1022,
+	spec_version: 1023,
 	impl_version: 1,
 	#[cfg(not(feature = "disable-runtime-api"))]
 	apis: RUNTIME_API_VERSIONS,
diff --git a/runtime/integration-tests/Cargo.toml b/runtime/integration-tests/Cargo.toml
index c35567d1d3..3a08c7e180 100644
--- a/runtime/integration-tests/Cargo.toml
+++ b/runtime/integration-tests/Cargo.toml
@@ -88,12 +88,14 @@ cfg-traits = { path = "../../libs/traits" }
 cfg-types = { path = "../../libs/types" }
 cfg-utils = { path = "../../libs/utils" }
 
+ethabi = { version = "16.0", default-features = false }
 ethereum = { version = "0.14.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" }
 pallet-evm-chain-id = { git = "https://github.com/PureStake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.38" }
 
+axelar-gateway-precompile = { path = "../../pallets/liquidity-pools-gateway/axelar-gateway-precompile" }
 liquidity-pools-gateway-routers = { path = "../../pallets/liquidity-pools-gateway/routers" }
 pallet-block-rewards = { path = "../../pallets/block-rewards" }
 pallet-ethereum-transaction = { path = "../../pallets/ethereum-transaction" }
@@ -166,6 +168,7 @@ std = [
   "pallet-collective/std",
   "pallet-democracy/std",
   "pallet-preimage/std",
+  "ethabi/std",
 ]
 
 runtime-benchmarks = [
diff --git a/runtime/integration-tests/src/ethereum_transaction/pallet.rs b/runtime/integration-tests/src/evm/ethereum_transaction.rs
similarity index 99%
rename from runtime/integration-tests/src/ethereum_transaction/pallet.rs
rename to runtime/integration-tests/src/evm/ethereum_transaction.rs
index 92713adfac..b29f0a5681 100644
--- a/runtime/integration-tests/src/ethereum_transaction/pallet.rs
+++ b/runtime/integration-tests/src/evm/ethereum_transaction.rs
@@ -25,7 +25,6 @@ use crate::{
 		AccountId, CouncilCollective, FastTrackVotingPeriod, MinimumDeposit, Runtime, RuntimeCall,
 		RuntimeEvent, PARA_ID,
 	},
-	ethereum_transaction::pallet,
 	utils::{
 		env,
 		env::{ChainState, EventRange, TestEnv},
diff --git a/runtime/integration-tests/src/ethereum_transaction/mod.rs b/runtime/integration-tests/src/evm/mod.rs
similarity index 93%
rename from runtime/integration-tests/src/ethereum_transaction/mod.rs
rename to runtime/integration-tests/src/evm/mod.rs
index 0ca88fbd73..99172b9fdb 100644
--- a/runtime/integration-tests/src/ethereum_transaction/mod.rs
+++ b/runtime/integration-tests/src/evm/mod.rs
@@ -10,4 +10,5 @@
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 // GNU General Public License for more details.
 
-mod pallet;
+mod ethereum_transaction;
+mod precompile;
diff --git a/runtime/integration-tests/src/evm/precompile.rs b/runtime/integration-tests/src/evm/precompile.rs
new file mode 100644
index 0000000000..2abb953735
--- /dev/null
+++ b/runtime/integration-tests/src/evm/precompile.rs
@@ -0,0 +1,232 @@
+// 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 std::collections::BTreeMap;
+
+use axelar_gateway_precompile::SourceConverter;
+use cfg_primitives::{Balance, PoolId, TrancheId, CFG};
+use cfg_traits::{ethereum::EthereumTransactor, liquidity_pools::Codec};
+use cfg_types::{
+	domain_address::{Domain, DomainAddress},
+	fixed_point::Rate,
+	tokens::{CurrencyId, CustomMetadata, GeneralCurrencyIndex},
+};
+use codec::Encode;
+use ethabi::{Contract, Function, Param, ParamType, Token};
+use ethereum::{LegacyTransaction, TransactionAction, TransactionSignature, TransactionV2};
+use frame_support::{assert_err, assert_ok, dispatch::RawOrigin};
+use fudge::primitives::Chain;
+use hex::ToHex;
+use orml_traits::{asset_registry::AssetMetadata, MultiCurrency};
+use pallet_evm::{AddressMapping, FeeCalculator};
+use pallet_liquidity_pools::Message;
+use runtime_common::{account_conversion::AccountConverter, evm::precompile::LP_AXELAR_GATEWAY};
+use sp_core::{Get, H160, H256, U256};
+use sp_runtime::traits::{BlakeTwo256, Hash};
+use tokio::runtime::Handle;
+use xcm::{v3::MultiLocation, VersionedMultiLocation};
+
+use crate::{
+	chain::centrifuge::{
+		AccountId, CouncilCollective, FastTrackVotingPeriod, MinimumDeposit, Runtime, RuntimeCall,
+		RuntimeEvent, RuntimeOrigin, PARA_ID,
+	},
+	evm::ethereum_transaction::TEST_CONTRACT_CODE,
+	utils::{
+		env,
+		env::{ChainState, EventRange, TestEnv},
+		evm::{deploy_contract, mint_balance_into_derived_account},
+	},
+};
+
+#[tokio::test]
+async fn axelar_precompile_execute() {
+	let mut env = env::test_env_default(Handle::current());
+
+	env.evolve().unwrap();
+
+	let currency_id = CurrencyId::ForeignAsset(123456);
+
+	let sender_address = H160::from_low_u64_be(1_000_002);
+
+	mint_balance_into_derived_account(&mut env, sender_address, 1_000_000 * CFG);
+
+	let derived_sender_account = env
+		.with_state(Chain::Para(PARA_ID), || {
+			<Runtime as pallet_evm::Config>::AddressMapping::into_account_id(sender_address)
+		})
+		.unwrap();
+
+	let receiver_address = H160::from_low_u64_be(1_000_003);
+
+	let derived_receiver_account = env
+		.with_state(Chain::Para(PARA_ID), || {
+			<Runtime as pallet_evm::Config>::AddressMapping::into_account_id(receiver_address)
+		})
+		.unwrap();
+
+	env.with_state(Chain::Para(PARA_ID), || {
+		let derived_receiver_balance =
+			orml_tokens::Pallet::<Runtime>::free_balance(currency_id, &derived_receiver_account);
+
+		assert_eq!(derived_receiver_balance, 0)
+	})
+	.unwrap();
+
+	let source_address = H160::from_low_u64_be(1111);
+	let evm_chain_name = String::from("Ethereum");
+	let evm_chain_id = 0;
+
+	let currency_metadata = AssetMetadata {
+		decimals: 18,
+		name: "Test".into(),
+		symbol: "TST".into(),
+		existential_deposit: 1_000_000,
+		location: Some(VersionedMultiLocation::V3(MultiLocation::here())),
+		additional: CustomMetadata {
+			transferability: Default::default(),
+			mintable: true,
+			permissioned: false,
+			pool_currency: false,
+		},
+	};
+
+	env.with_mut_state(Chain::Para(PARA_ID), || {
+		orml_asset_registry::Pallet::<Runtime>::register_asset(
+			RuntimeOrigin::root(),
+			currency_metadata,
+			Some(currency_id),
+		)
+		.unwrap();
+
+		orml_tokens::Pallet::<Runtime>::deposit(
+			currency_id,
+			&derived_sender_account,
+			1_000_000_000_000 * 10u128.saturating_pow(18),
+		)
+		.unwrap();
+	})
+	.unwrap();
+
+	let general_currency_id = env
+		.with_state(Chain::Para(PARA_ID), || {
+			pallet_liquidity_pools::Pallet::<Runtime>::try_get_general_index(currency_id).unwrap()
+		})
+		.unwrap();
+
+	let transfer_amount = 100;
+	let msg = Message::<Domain, PoolId, TrancheId, Balance, Rate>::Transfer {
+		currency: general_currency_id,
+		sender: derived_sender_account.clone().into(),
+		receiver: derived_receiver_account.clone().into(),
+		amount: transfer_amount,
+	};
+
+	env.with_mut_state(Chain::Para(PARA_ID), || {
+		axelar_gateway_precompile::Pallet::<Runtime>::set_gateway(
+			RuntimeOrigin::root(),
+			sender_address,
+		)
+		.unwrap();
+
+		axelar_gateway_precompile::Pallet::<Runtime>::set_converter(
+			RuntimeOrigin::root(),
+			BlakeTwo256::hash(evm_chain_name.as_bytes()),
+			SourceConverter {
+				domain: Domain::EVM(evm_chain_id),
+			},
+		)
+		.unwrap();
+
+		pallet_liquidity_pools_gateway::Pallet::<Runtime>::add_instance(
+			RuntimeOrigin::root(),
+			DomainAddress::EVM(evm_chain_id, source_address.0),
+		)
+		.unwrap();
+	});
+
+	let command_id = H256::from_low_u64_be(5678);
+
+	let test_input = Contract {
+		constructor: None,
+		functions: BTreeMap::<String, Vec<Function>>::from([(
+			"execute".into(),
+			vec![Function {
+				name: "execute".into(),
+				inputs: vec![
+					Param {
+						name: "commandId".into(),
+						kind: ParamType::FixedBytes(32),
+						internal_type: None,
+					},
+					Param {
+						name: "sourceChain".into(),
+						kind: ParamType::String,
+						internal_type: None,
+					},
+					Param {
+						name: "sourceAddress".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("execute".into())
+	.map_err(|_| "cannot retrieve test contract function")
+	.unwrap()
+	.encode_input(&[
+		Token::FixedBytes(command_id.0.to_vec()),
+		Token::String(evm_chain_name),
+		Token::String(String::from_utf8(source_address.as_fixed_bytes().to_vec()).unwrap()),
+		Token::Bytes(msg.serialize()),
+	])
+	.map_err(|_| "cannot encode input for test contract function")
+	.unwrap();
+
+	env.with_mut_state(Chain::Para(PARA_ID), || {
+		assert_ok!(pallet_evm::Pallet::<Runtime>::call(
+			RawOrigin::Signed(derived_sender_account.clone()).into(),
+			sender_address,
+			LP_AXELAR_GATEWAY.into(),
+			test_input.to_vec(),
+			U256::from(0),
+			0x100000,
+			U256::from(1_000_000_000),
+			None,
+			Some(U256::from(0)),
+			Vec::new(),
+		));
+	})
+	.unwrap();
+
+	env.with_state(Chain::Para(PARA_ID), || {
+		let derived_receiver_balance =
+			orml_tokens::Pallet::<Runtime>::free_balance(currency_id, &derived_receiver_account);
+
+		assert_eq!(derived_receiver_balance, transfer_amount)
+	})
+	.unwrap();
+}
diff --git a/runtime/integration-tests/src/lib.rs b/runtime/integration-tests/src/lib.rs
index c2842655c9..25636495d4 100644
--- a/runtime/integration-tests/src/lib.rs
+++ b/runtime/integration-tests/src/lib.rs
@@ -14,7 +14,7 @@
 #![cfg(test)]
 #![allow(unused)]
 
-mod ethereum_transaction;
+mod evm;
 mod liquidity_pools;
 mod pools;
 mod rewards;