#![cfg_attr(not(feature = "std"), no_std)]
use cfg_traits::{
ethereum::EthereumTransactor,
liquidity_pools::{MessageReceiver, MessageSender},
PreConditions,
};
use cfg_types::{domain_address::DomainAddress, EVMChainId};
use ethabi::{Contract, Function, Param, ParamType, Token};
use fp_evm::PrecompileHandle;
use frame_support::{
pallet_prelude::*,
weights::{constants::RocksDbWeight, Weight},
BoundedVec,
};
use frame_system::pallet_prelude::*;
pub use pallet::*;
use precompile_utils::prelude::*;
use scale_info::prelude::{format, string::String};
use sp_core::{H160, H256, U256};
use sp_std::{boxed::Box, collections::btree_map::BTreeMap, vec, vec::Vec};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
const MAX_AXELAR_EVM_CHAIN_SIZE: u32 = 16;
const MAX_SOURCE_CHAIN_BYTES: u32 = 128;
const MAX_SOURCE_ADDRESS_BYTES: u32 = 42;
const MAX_TOKEN_SYMBOL_BYTES: u32 = 32;
const MAX_PAYLOAD_BYTES: u32 = 1024;
const EVM_ADDRESS_LEN: usize = 20;
pub type ChainName = BoundedVec<u8, ConstU32<MAX_AXELAR_EVM_CHAIN_SIZE>>;
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
pub enum AxelarId {
Evm(EVMChainId),
}
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
pub struct AxelarConfig {
pub liquidity_pools_contract_address: H160,
pub domain: DomainConfig,
}
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
pub enum DomainConfig {
Evm(EvmConfig),
}
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
pub struct EvmConfig {
pub chain_id: EVMChainId,
pub target_contract_address: H160,
pub target_contract_hash: H256,
pub fee_values: FeeValues,
}
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
pub struct FeeValues {
pub value: U256,
pub gas_price: U256,
pub gas_limit: U256,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type Receiver: MessageReceiver<
Middleware = Self::Middleware,
Origin = DomainAddress,
Message = Vec<u8>,
>;
type Middleware: From<AxelarId>;
type Transactor: EthereumTransactor;
type EvmAccountCodeChecker: PreConditions<(H160, H256), Result = bool>;
}
#[pallet::storage]
pub type Configuration<T: Config> = StorageMap<_, Twox64Concat, ChainName, AxelarConfig>;
#[pallet::storage]
pub type ChainNameById<T: Config> = StorageMap<_, Twox64Concat, AxelarId, ChainName>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
ConfigSet {
name: ChainName,
config: Box<AxelarConfig>,
},
}
#[pallet::error]
pub enum Error<T> {
RouterConfigurationNotFound,
ContractCodeMismatch,
SourceChainTooLong,
InvalidSourceAddress,
ContractCallerMismatch,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(Weight::from_parts(50_000_000, 512).saturating_add(RocksDbWeight::get().writes(2)))]
#[pallet::call_index(0)]
pub fn set_config(
origin: OriginFor<T>,
chain_name: ChainName,
config: Box<AxelarConfig>,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
match &config.domain {
DomainConfig::Evm(evm_config) => {
ensure!(
T::EvmAccountCodeChecker::check((
evm_config.target_contract_address,
evm_config.target_contract_hash,
)),
Error::<T>::ContractCodeMismatch
);
ChainNameById::<T>::insert(
AxelarId::Evm(evm_config.chain_id),
chain_name.clone(),
);
}
}
Configuration::<T>::insert(chain_name.clone(), config.clone());
Self::deposit_event(Event::<T>::ConfigSet {
name: chain_name,
config,
});
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn receive(
caller: H160,
source_chain: &[u8],
source_address: &[u8],
payload: &[u8],
) -> DispatchResult {
let chain_name: ChainName = source_chain
.to_vec()
.try_into()
.map_err(|_| Error::<T>::SourceChainTooLong)?;
let config = Configuration::<T>::get(chain_name)
.ok_or(Error::<T>::RouterConfigurationNotFound)?;
ensure!(
caller == config.liquidity_pools_contract_address,
Error::<T>::ContractCallerMismatch,
);
match config.domain {
DomainConfig::Evm(EvmConfig { chain_id, .. }) => {
let source_address_bytes = decode_var_source::<EVM_ADDRESS_LEN>(source_address)
.ok_or(Error::<T>::InvalidSourceAddress)?;
T::Receiver::receive(
AxelarId::Evm(chain_id).into(),
DomainAddress::Evm(chain_id, source_address_bytes.into()),
payload.to_vec(),
)
}
}
}
}
#[precompile_utils::precompile]
impl<T: Config> Pallet<T> {
#[precompile::public("execute(bytes32,string,string,bytes)")]
fn execute(
handle: &mut impl PrecompileHandle,
_command_id: H256,
source_chain: BoundedString<ConstU32<MAX_SOURCE_CHAIN_BYTES>>,
source_address: BoundedString<ConstU32<MAX_SOURCE_ADDRESS_BYTES>>,
payload: BoundedBytes<ConstU32<MAX_PAYLOAD_BYTES>>,
) -> EvmResult {
Self::receive(
handle.context().caller,
source_chain.as_bytes(),
source_address.as_bytes(),
payload.as_bytes(),
)
.map_err(|e| TryDispatchError::Substrate(e).into())
}
#[precompile::public("executeWithToken(bytes32,string,string,bytes,string,uint256)")]
fn execute_with_token(
_handle: &mut impl PrecompileHandle,
_command_id: H256,
_source_chain: BoundedString<ConstU32<MAX_SOURCE_CHAIN_BYTES>>,
_source_address: BoundedString<ConstU32<MAX_SOURCE_ADDRESS_BYTES>>,
_payload: BoundedBytes<ConstU32<MAX_PAYLOAD_BYTES>>,
_token_symbol: BoundedString<ConstU32<MAX_TOKEN_SYMBOL_BYTES>>,
_amount: U256,
) -> EvmResult {
Ok(())
}
}
impl<T: Config> MessageSender for Pallet<T> {
type Message = Vec<u8>;
type Middleware = AxelarId;
type Origin = DomainAddress;
fn send(
axelar_id: AxelarId,
origin: Self::Origin,
message: Self::Message,
) -> DispatchResult {
let chain_name = ChainNameById::<T>::get(axelar_id)
.ok_or(Error::<T>::RouterConfigurationNotFound)?;
let config = Configuration::<T>::get(&chain_name)
.ok_or(Error::<T>::RouterConfigurationNotFound)?;
match config.domain {
DomainConfig::Evm(evm_config) => {
let message = wrap_into_axelar_msg(
message,
chain_name.into_inner(),
config.liquidity_pools_contract_address,
)
.map_err(DispatchError::Other)?;
T::Transactor::call(
origin.h160(),
evm_config.target_contract_address,
message.as_slice(),
evm_config.fee_values.value,
evm_config.fee_values.gas_price,
evm_config.fee_values.gas_limit,
)
.map(|_| ())
.map_err(|e| e.error)
}
}
}
}
}
pub fn wrap_into_axelar_msg(
serialized_msg: Vec<u8>,
target_chain: Vec<u8>,
target_contract: H160,
) -> Result<Vec<u8>, &'static str> {
const AXELAR_FUNCTION_NAME: &str = "callContract";
const AXELAR_DESTINATION_CHAIN_PARAM: &str = "destinationChain";
const AXELAR_DESTINATION_CONTRACT_ADDRESS_PARAM: &str = "destinationContractAddress";
const AXELAR_PAYLOAD_PARAM: &str = "payload";
#[allow(deprecated)]
let encoded_axelar_contract = Contract {
constructor: None,
functions: BTreeMap::<String, Vec<Function>>::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: Some(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(
String::from_utf8(target_chain).map_err(|_| "target chain conversion error")?,
),
Token::String(format!("0x{}", hex::encode(target_contract.0))),
Token::Bytes(serialized_msg),
])
.map_err(|_| "cannot encode input for Axelar contract function")?;
Ok(encoded_axelar_contract)
}
pub fn decode_var_source<const EXPECTED_SOURCE_ADDRESS_SIZE: usize>(
source_address: &[u8],
) -> Option<[u8; EXPECTED_SOURCE_ADDRESS_SIZE]> {
const HEX_PREFIX: &str = "0x";
let mut address = [0u8; EXPECTED_SOURCE_ADDRESS_SIZE];
if source_address.len() == EXPECTED_SOURCE_ADDRESS_SIZE {
address.copy_from_slice(source_address);
return Some(address);
}
let try_bytes = match sp_std::str::from_utf8(source_address) {
Ok(res) => res.as_bytes(),
Err(_) => source_address,
};
let bytes = match hex::decode(try_bytes) {
Ok(res) => Some(res),
Err(_) => {
let res = try_bytes.strip_prefix(HEX_PREFIX.as_bytes())?;
hex::decode(res).ok()
}
}?;
if bytes.len() == EXPECTED_SOURCE_ADDRESS_SIZE {
address.copy_from_slice(bytes.as_slice());
Some(address)
} else {
None
}
}
#[cfg(test)]
mod test_decode_var_source {
const EXPECTED: usize = 20;
use super::*;
#[test]
fn success() {
assert!(decode_var_source::<EXPECTED>(&[1; 20]).is_some());
assert!(decode_var_source::<EXPECTED>(
"d47ed02acbbb66ee8a3fe0275bd98add0aa607c3".as_bytes()
)
.is_some());
assert!(decode_var_source::<EXPECTED>(
"0xd47ed02acbbb66ee8a3fe0275bd98add0aa607c3".as_bytes()
)
.is_some());
}
}