#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(any(test, feature = "runtime-benchmarks"))]
pub mod benchmarks;
#[cfg(test)]
pub mod mock;
#[cfg(test)]
pub mod tests;
pub mod weights;
mod evm;
pub use pallet::*;
pub use weights::WeightInfo;
use self::evm::EvmCaller;
use ethereum_types::{H160, U256};
use frame_support::pallet;
use frame_support::pallet_prelude::*;
use frame_support::traits::Contains;
use frame_system::pallet_prelude::*;
use xcm::latest::{
Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, Result as XcmResult,
XcmContext,
};
use xcm_executor::traits::Error as MatchError;
const FOREIGN_ASSETS_PREFIX: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
pub trait ForeignAssetCreatedHook<ForeignAsset> {
fn on_asset_created(foreign_asset: &ForeignAsset, asset_id: &AssetId);
}
impl<ForeignAsset> ForeignAssetCreatedHook<ForeignAsset> for () {
fn on_asset_created(_foreign_asset: &ForeignAsset, _asset_id: &AssetId) {}
}
pub(crate) struct ForeignAssetsMatcher<T>(core::marker::PhantomData<T>);
impl<T: crate::Config> ForeignAssetsMatcher<T> {
fn match_asset(asset: &Asset) -> Result<(H160, U256, AssetStatus), MatchError> {
let (amount, location) = match (&asset.fun, &asset.id) {
(Fungibility::Fungible(ref amount), XcmAssetId(ref location)) => (amount, location),
_ => return Err(MatchError::AssetNotHandled),
};
if let Some((asset_id, asset_status)) = AssetsByLocation::<T>::get(&location) {
Ok((
Pallet::<T>::contract_address_from_asset_id(asset_id),
U256::from(*amount),
asset_status,
))
} else {
Err(MatchError::AssetNotHandled)
}
}
}
#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
pub enum AssetStatus {
Active,
FrozenXcmDepositAllowed,
FrozenXcmDepositForbidden,
}
#[pallet]
pub mod pallet {
use super::*;
use pallet_evm::{GasWeightMapping, Runner};
use sp_runtime::traits::{AccountIdConversion, Convert};
use xcm_executor::traits::ConvertLocation;
use xcm_executor::traits::Error as MatchError;
use xcm_executor::AssetsInHolding;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(PhantomData<T>);
pub const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"forgasst");
#[pallet::config]
pub trait Config: frame_system::Config + pallet_evm::Config {
type AccountIdToH160: Convert<Self::AccountId, H160>;
type AssetIdFilter: Contains<AssetId>;
type EvmRunner: Runner<Self>;
type ForeignAssetCreatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type ForeignAssetFreezerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type ForeignAssetModifierOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type ForeignAssetUnfreezerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type OnForeignAssetCreated: ForeignAssetCreatedHook<Location>;
type MaxForeignAssets: Get<u32>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
type XcmLocationToH160: ConvertLocation<H160>;
}
pub type AssetBalance = U256;
pub type AssetId = u128;
#[pallet::error]
pub enum Error<T> {
AssetAlreadyExists,
AssetAlreadyFrozen,
AssetDoesNotExist,
AssetIdFiltered,
AssetNotFrozen,
CorruptedStorageOrphanLocation,
Erc20ContractCreationFail,
EvmCallPauseFail,
EvmCallUnpauseFail,
EvmInternalError,
InvalidSymbol,
InvalidTokenName,
LocationAlreadyExists,
TooManyForeignAssets,
}
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
ForeignAssetCreated {
contract_address: H160,
asset_id: AssetId,
xcm_location: Location,
},
ForeignAssetXcmLocationChanged {
asset_id: AssetId,
new_xcm_location: Location,
},
ForeignAssetFrozen {
asset_id: AssetId,
xcm_location: Location,
},
ForeignAssetUnfrozen {
asset_id: AssetId,
xcm_location: Location,
},
}
#[pallet::storage]
#[pallet::getter(fn assets_by_id)]
pub type AssetsById<T: Config> =
CountedStorageMap<_, Blake2_128Concat, AssetId, Location, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn assets_by_location)]
pub type AssetsByLocation<T: Config> =
StorageMap<_, Blake2_128Concat, Location, (AssetId, AssetStatus)>;
impl<T: Config> Pallet<T> {
#[inline]
pub fn account_id() -> H160 {
let account_id: T::AccountId = PALLET_ID.into_account_truncating();
T::AccountIdToH160::convert(account_id)
}
#[inline]
pub(crate) fn contract_address_from_asset_id(asset_id: AssetId) -> H160 {
let mut buffer = [0u8; 20];
buffer[..4].copy_from_slice(&FOREIGN_ASSETS_PREFIX);
buffer[4..].copy_from_slice(&asset_id.to_be_bytes());
H160(buffer)
}
pub fn mint_into(
asset_id: AssetId,
beneficiary: T::AccountId,
amount: U256,
) -> Result<(), evm::EvmError> {
frame_support::storage::with_storage_layer(|| {
EvmCaller::<T>::erc20_mint_into(
Self::contract_address_from_asset_id(asset_id),
T::AccountIdToH160::convert(beneficiary),
amount,
)
})
.map_err(Into::into)
}
pub fn weight_of_erc20_burn() -> Weight {
T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
}
pub fn weight_of_erc20_mint() -> Weight {
T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
}
pub fn weight_of_erc20_transfer() -> Weight {
T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
}
#[cfg(feature = "runtime-benchmarks")]
pub fn set_asset(asset_location: Location, asset_id: AssetId) {
AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
AssetsById::<T>::insert(&asset_id, asset_location);
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
pub fn create_foreign_asset(
origin: OriginFor<T>,
asset_id: AssetId,
xcm_location: Location,
decimals: u8,
symbol: BoundedVec<u8, ConstU32<256>>,
name: BoundedVec<u8, ConstU32<256>>,
) -> DispatchResult {
T::ForeignAssetCreatorOrigin::ensure_origin(origin)?;
ensure!(
!AssetsById::<T>::contains_key(&asset_id),
Error::<T>::AssetAlreadyExists
);
ensure!(
!AssetsByLocation::<T>::contains_key(&xcm_location),
Error::<T>::LocationAlreadyExists
);
ensure!(
AssetsById::<T>::count() < T::MaxForeignAssets::get(),
Error::<T>::TooManyForeignAssets
);
ensure!(
T::AssetIdFilter::contains(&asset_id),
Error::<T>::AssetIdFiltered
);
let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
AssetsById::<T>::insert(&asset_id, &xcm_location);
AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
T::OnForeignAssetCreated::on_asset_created(&xcm_location, &asset_id);
Self::deposit_event(Event::ForeignAssetCreated {
contract_address,
asset_id,
xcm_location,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
pub fn change_xcm_location(
origin: OriginFor<T>,
asset_id: AssetId,
new_xcm_location: Location,
) -> DispatchResult {
T::ForeignAssetModifierOrigin::ensure_origin(origin)?;
let previous_location =
AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
ensure!(
!AssetsByLocation::<T>::contains_key(&new_xcm_location),
Error::<T>::LocationAlreadyExists
);
let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_location)
.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
AssetsById::<T>::insert(&asset_id, &new_xcm_location);
AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
asset_id,
new_xcm_location,
});
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
pub fn freeze_foreign_asset(
origin: OriginFor<T>,
asset_id: AssetId,
allow_xcm_deposit: bool,
) -> DispatchResult {
T::ForeignAssetFreezerOrigin::ensure_origin(origin)?;
let xcm_location =
AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
ensure!(
asset_status == AssetStatus::Active,
Error::<T>::AssetAlreadyFrozen
);
EvmCaller::<T>::erc20_pause(asset_id)?;
let new_asset_status = if allow_xcm_deposit {
AssetStatus::FrozenXcmDepositAllowed
} else {
AssetStatus::FrozenXcmDepositForbidden
};
AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
Self::deposit_event(Event::ForeignAssetFrozen {
asset_id,
xcm_location,
});
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
T::ForeignAssetUnfreezerOrigin::ensure_origin(origin)?;
let xcm_location =
AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
ensure!(
asset_status == AssetStatus::FrozenXcmDepositAllowed
|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
Error::<T>::AssetNotFrozen
);
EvmCaller::<T>::erc20_unpause(asset_id)?;
AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
Self::deposit_event(Event::ForeignAssetUnfrozen {
asset_id,
xcm_location,
});
Ok(())
}
}
impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
let (contract_address, amount, asset_status) =
ForeignAssetsMatcher::<T>::match_asset(what)?;
if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
return Err(MatchError::AssetNotHandled.into());
}
let beneficiary = T::XcmLocationToH160::convert_location(who)
.ok_or(MatchError::AccountIdConversionFailed)?;
frame_support::storage::with_storage_layer(|| {
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
})?;
Ok(())
}
fn internal_transfer_asset(
asset: &Asset,
from: &Location,
to: &Location,
_context: &XcmContext,
) -> Result<AssetsInHolding, XcmError> {
let (contract_address, amount, asset_status) =
ForeignAssetsMatcher::<T>::match_asset(asset)?;
if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
asset_status
{
return Err(MatchError::AssetNotHandled.into());
}
let from = T::XcmLocationToH160::convert_location(from)
.ok_or(MatchError::AccountIdConversionFailed)?;
let to = T::XcmLocationToH160::convert_location(to)
.ok_or(MatchError::AccountIdConversionFailed)?;
frame_support::storage::with_storage_layer(|| {
EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
})?;
Ok(asset.clone().into())
}
fn withdraw_asset(
what: &Asset,
who: &Location,
_context: Option<&XcmContext>,
) -> Result<AssetsInHolding, XcmError> {
let (contract_address, amount, asset_status) =
ForeignAssetsMatcher::<T>::match_asset(what)?;
let who = T::XcmLocationToH160::convert_location(who)
.ok_or(MatchError::AccountIdConversionFailed)?;
if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
asset_status
{
return Err(MatchError::AssetNotHandled.into());
}
frame_support::storage::with_storage_layer(|| {
EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
})?;
Ok(what.clone().into())
}
}
impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
fn convert(location: &Location) -> Option<AssetId> {
AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
}
fn convert_back(asset_id: &AssetId) -> Option<Location> {
AssetsById::<T>::get(asset_id)
}
}
}