use crate::{AssetId, Error, Pallet};
use ethereum_types::{BigEndianHash, H160, H256, U256};
use fp_evm::{ExitReason, ExitSucceed};
use frame_support::ensure;
use frame_support::pallet_prelude::Weight;
use pallet_evm::{GasWeightMapping, Runner};
use precompile_utils::prelude::*;
use precompile_utils::solidity::codec::{Address, BoundedString};
use precompile_utils::solidity::Codec;
use precompile_utils_macro::keccak256;
use sp_runtime::traits::ConstU32;
use sp_runtime::{format, DispatchError, SaturatedConversion};
use sp_std::vec::Vec;
use xcm::latest::Error as XcmError;
const ERC20_CALL_MAX_CALLDATA_SIZE: usize = 4 + 32 + 32; const ERC20_CREATE_MAX_CALLDATA_SIZE: usize = 16 * 1024; const ERC20_CREATE_GAS_LIMIT: u64 = 3_600_000; pub(crate) const ERC20_BURN_FROM_GAS_LIMIT: u64 = 160_000; pub(crate) const ERC20_MINT_INTO_GAS_LIMIT: u64 = 160_000; const ERC20_PAUSE_GAS_LIMIT: u64 = 160_000; pub(crate) const ERC20_TRANSFER_GAS_LIMIT: u64 = 160_000; pub(crate) const ERC20_APPROVE_GAS_LIMIT: u64 = 160_000; const ERC20_UNPAUSE_GAS_LIMIT: u64 = 160_000; #[derive(Debug)]
pub enum EvmError {
BurnFromFail(String),
ContractReturnInvalidValue,
DispatchError(DispatchError),
EvmCallFail(String),
MintIntoFail(String),
TransferFail(String),
}
impl From<DispatchError> for EvmError {
fn from(e: DispatchError) -> Self {
Self::DispatchError(e)
}
}
impl From<EvmError> for XcmError {
fn from(error: EvmError) -> XcmError {
match error {
EvmError::BurnFromFail(err) => {
log::debug!("BurnFromFail error: {:?}", err);
XcmError::FailedToTransactAsset("Erc20 contract call burnFrom fail")
}
EvmError::ContractReturnInvalidValue => {
XcmError::FailedToTransactAsset("Erc20 contract return invalid value")
}
EvmError::DispatchError(err) => {
log::debug!("dispatch error: {:?}", err);
Self::FailedToTransactAsset("storage layer error")
}
EvmError::EvmCallFail(err) => {
log::debug!("EvmCallFail error: {:?}", err);
XcmError::FailedToTransactAsset("Fail to call erc20 contract")
}
EvmError::MintIntoFail(err) => {
log::debug!("MintIntoFail error: {:?}", err);
XcmError::FailedToTransactAsset("Erc20 contract call mintInto fail+")
}
EvmError::TransferFail(err) => {
log::debug!("TransferFail error: {:?}", err);
XcmError::FailedToTransactAsset("Erc20 contract call transfer fail")
}
}
}
}
#[derive(Codec)]
#[cfg_attr(test, derive(Debug))]
struct ForeignErc20ConstructorArgs {
owner: Address,
decimals: u8,
symbol: BoundedString<ConstU32<64>>,
token_name: BoundedString<ConstU32<256>>,
}
pub(crate) struct EvmCaller<T: crate::Config>(core::marker::PhantomData<T>);
impl<T: crate::Config> EvmCaller<T> {
pub(crate) fn erc20_create(
asset_id: AssetId,
decimals: u8,
symbol: &str,
token_name: &str,
) -> Result<H160, Error<T>> {
let mut init = Vec::with_capacity(ERC20_CREATE_MAX_CALLDATA_SIZE);
init.extend_from_slice(include_bytes!("../resources/foreign_erc20_initcode.bin"));
let args = ForeignErc20ConstructorArgs {
owner: Pallet::<T>::account_id().into(),
decimals,
symbol: symbol.into(),
token_name: token_name.into(),
};
let encoded_args = precompile_utils::solidity::codec::Writer::new()
.write(args)
.build();
init.extend_from_slice(&encoded_args[32..]);
let contract_adress = Pallet::<T>::contract_address_from_asset_id(asset_id);
let exec_info = T::EvmRunner::create_force_address(
Pallet::<T>::account_id(),
init,
U256::default(),
ERC20_CREATE_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
None,
None,
&<T as pallet_evm::Config>::config(),
contract_adress,
)
.map_err(|err| {
log::debug!("erc20_create (error): {:?}", err.error.into());
Error::<T>::Erc20ContractCreationFail
})?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
Error::Erc20ContractCreationFail
);
Ok(contract_adress)
}
pub(crate) fn erc20_mint_into(
erc20_contract_address: H160,
beneficiary: H160,
amount: U256,
) -> Result<(), EvmError> {
let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
input.extend_from_slice(&keccak256!("mintInto(address,uint256)")[..4]);
input.extend_from_slice(H256::from(beneficiary).as_bytes());
input.extend_from_slice(H256::from_uint(&amount).as_bytes());
let weight_limit: Weight =
T::GasWeightMapping::gas_to_weight(ERC20_MINT_INTO_GAS_LIMIT, true);
let exec_info = T::EvmRunner::call(
Pallet::<T>::account_id(),
erc20_contract_address,
input,
U256::default(),
ERC20_MINT_INTO_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| EvmError::MintIntoFail(format!("{:?}", err.error.into())))?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
EvmError::MintIntoFail(err)
}
);
Ok(())
}
pub(crate) fn erc20_transfer(
erc20_contract_address: H160,
from: H160,
to: H160,
amount: U256,
) -> Result<(), EvmError> {
let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
input.extend_from_slice(&keccak256!("transfer(address,uint256)")[..4]);
input.extend_from_slice(H256::from(to).as_bytes());
input.extend_from_slice(H256::from_uint(&amount).as_bytes());
let weight_limit: Weight =
T::GasWeightMapping::gas_to_weight(ERC20_TRANSFER_GAS_LIMIT, true);
let exec_info = T::EvmRunner::call(
from,
erc20_contract_address,
input,
U256::default(),
ERC20_TRANSFER_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| EvmError::TransferFail(format!("{:?}", err.error.into())))?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
EvmError::TransferFail(err)
}
);
let mut bytes = [0u8; 32];
U256::from(1).to_big_endian(&mut bytes);
ensure!(
!exec_info.value.is_empty() && exec_info.value == bytes,
EvmError::ContractReturnInvalidValue
);
Ok(())
}
pub(crate) fn erc20_approve(
erc20_contract_address: H160,
owner: H160,
spender: H160,
amount: U256,
) -> Result<(), EvmError> {
let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
input.extend_from_slice(&keccak256!("approve(address,uint256)")[..4]);
input.extend_from_slice(H256::from(spender).as_bytes());
input.extend_from_slice(H256::from_uint(&amount).as_bytes());
let weight_limit: Weight =
T::GasWeightMapping::gas_to_weight(ERC20_APPROVE_GAS_LIMIT, true);
let exec_info = T::EvmRunner::call(
owner,
erc20_contract_address,
input,
U256::default(),
ERC20_APPROVE_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
EvmError::EvmCallFail(err)
}
);
Ok(())
}
pub(crate) fn erc20_burn_from(
erc20_contract_address: H160,
who: H160,
amount: U256,
) -> Result<(), EvmError> {
let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
input.extend_from_slice(&keccak256!("burnFrom(address,uint256)")[..4]);
input.extend_from_slice(H256::from(who).as_bytes());
input.extend_from_slice(H256::from_uint(&amount).as_bytes());
let weight_limit: Weight =
T::GasWeightMapping::gas_to_weight(ERC20_BURN_FROM_GAS_LIMIT, true);
let exec_info = T::EvmRunner::call(
Pallet::<T>::account_id(),
erc20_contract_address,
input,
U256::default(),
ERC20_BURN_FROM_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
EvmError::BurnFromFail(err)
}
);
Ok(())
}
pub(crate) fn erc20_pause(asset_id: AssetId) -> Result<(), Error<T>> {
let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
input.extend_from_slice(&keccak256!("pause()")[..4]);
let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(ERC20_PAUSE_GAS_LIMIT, true);
let exec_info = T::EvmRunner::call(
Pallet::<T>::account_id(),
Pallet::<T>::contract_address_from_asset_id(asset_id),
input,
U256::default(),
ERC20_PAUSE_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| {
log::debug!("erc20_pause (error): {:?}", err.error.into());
Error::<T>::EvmInternalError
})?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
log::debug!("erc20_pause (error): {:?}", err);
Error::<T>::EvmCallPauseFail
}
);
Ok(())
}
pub(crate) fn erc20_unpause(asset_id: AssetId) -> Result<(), Error<T>> {
let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
input.extend_from_slice(&keccak256!("unpause()")[..4]);
let weight_limit: Weight =
T::GasWeightMapping::gas_to_weight(ERC20_UNPAUSE_GAS_LIMIT, true);
let exec_info = T::EvmRunner::call(
Pallet::<T>::account_id(),
Pallet::<T>::contract_address_from_asset_id(asset_id),
input,
U256::default(),
ERC20_UNPAUSE_GAS_LIMIT,
None,
None,
None,
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| {
log::debug!("erc20_unpause (error): {:?}", err.error.into());
Error::<T>::EvmInternalError
})?;
ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
log::debug!("erc20_unpause (error): {:?}", err);
Error::<T>::EvmCallUnpauseFail
}
);
Ok(())
}
}
fn error_on_execution_failure(reason: &ExitReason, data: &[u8]) -> String {
match reason {
ExitReason::Succeed(_) => String::new(),
ExitReason::Error(err) => format!("evm error: {err:?}"),
ExitReason::Fatal(err) => format!("evm fatal: {err:?}"),
ExitReason::Revert(_) => extract_revert_message(data),
}
}
fn extract_revert_message(data: &[u8]) -> String {
const LEN_START: usize = 36;
const MESSAGE_START: usize = 68;
const BASE_MESSAGE: &str = "VM Exception while processing transaction: revert";
if data.len() <= MESSAGE_START {
return BASE_MESSAGE.into();
}
let message_len = U256::from(&data[LEN_START..MESSAGE_START]).saturated_into::<usize>();
let message_end = MESSAGE_START.saturating_add(message_len);
if data.len() < message_end {
return BASE_MESSAGE.into();
}
let body = &data[MESSAGE_START..message_end];
match core::str::from_utf8(body) {
Ok(reason) => format!("{BASE_MESSAGE} {reason}"),
Err(_) => BASE_MESSAGE.into(),
}
}