#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
pub mod impls;
pub mod types;
use crate::impls::{convert_to_balance, convert_to_erc20};
use alloy_sol_types::SolValue;
use anyhow::anyhow;
use codec::Decode;
use frame_support::{
ensure,
pallet_prelude::Weight,
traits::{
fungibles::{self, Mutate},
tokens::{fungible::Mutate as FungibleMutate, Preservation},
Currency, ExistenceRequirement,
},
};
use ismp::{
events::Meta,
router::{PostRequest, Request, Response, Timeout},
};
use sp_core::{Get, U256};
use token_gateway_primitives::{
token_gateway_id, token_governor_id, AssetMetadata, DeregisterAssets,
};
pub use types::*;
use alloc::{string::ToString, vec, vec::Vec};
use ismp::module::IsmpModule;
use primitive_types::H256;
pub use pallet::*;
const MIN_BALANCE: u128 = 1_000_000_000;
#[frame_support::pallet]
pub mod pallet {
use alloc::collections::BTreeMap;
use pallet_hyperbridge::PALLET_HYPERBRIDGE;
use sp_runtime::traits::AccountIdConversion;
use super::*;
use frame_support::{
pallet_prelude::*,
traits::{tokens::Preservation, Currency, ExistenceRequirement},
};
use frame_system::pallet_prelude::*;
use ismp::{
dispatcher::{DispatchPost, DispatchRequest, FeeMetadata, IsmpDispatcher},
host::StateMachine,
};
use pallet_hyperbridge::{SubstrateHostParams, VersionedHostParams};
use sp_runtime::traits::Zero;
use token_gateway_primitives::{GatewayAssetUpdate, RemoteERC6160AssetRegistration};
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config:
frame_system::Config + pallet_ismp::Config + pallet_hyperbridge::Config
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Dispatcher: IsmpDispatcher<Account = Self::AccountId, Balance = Self::Balance>;
type NativeCurrency: Currency<Self::AccountId>;
type AssetAdmin: Get<Self::AccountId>;
type Assets: fungibles::Mutate<Self::AccountId>
+ fungibles::Inspect<Self::AccountId>
+ fungibles::Create<Self::AccountId>
+ fungibles::metadata::Mutate<Self::AccountId>
+ fungibles::roles::Inspect<Self::AccountId>;
type NativeAssetId: Get<AssetId<Self>>;
type AssetIdFactory: CreateAssetId<AssetId<Self>>;
#[pallet::constant]
type Decimals: Get<u8>;
}
#[pallet::storage]
pub type SupportedAssets<T: Config> =
StorageMap<_, Blake2_128Concat, AssetId<T>, H256, OptionQuery>;
#[pallet::storage]
pub type LocalAssets<T: Config> = StorageMap<_, Identity, H256, AssetId<T>, OptionQuery>;
#[pallet::storage]
pub type Decimals<T: Config> = StorageMap<_, Blake2_128Concat, AssetId<T>, u8, OptionQuery>;
#[pallet::storage]
pub type TokenGatewayAddresses<T: Config> =
StorageMap<_, Blake2_128Concat, StateMachine, Vec<u8>, OptionQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
AssetTeleported {
from: T::AccountId,
to: H256,
amount: <<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance,
dest: StateMachine,
commitment: H256,
},
AssetReceived {
beneficiary: T::AccountId,
amount: <<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance,
source: StateMachine,
},
AssetRefunded {
beneficiary: T::AccountId,
amount: <<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance,
source: StateMachine,
},
ERC6160AssetRegistrationDispatched {
commitment: H256,
},
}
#[pallet::error]
pub enum Error<T> {
UnregisteredAsset,
AssetTeleportError,
CoprocessorNotConfigured,
DispatchError,
AssetCreationError,
AssetDecimalsNotFound,
NotInitialized,
UnknownAsset,
NotAssetOwner,
}
#[pallet::call]
impl<T: Config> Pallet<T>
where
<T as frame_system::Config>::AccountId: From<[u8; 32]>,
u128: From<<<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance>,
<T as pallet_ismp::Config>::Balance:
From<<<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance>,
<<T as Config>::Assets as fungibles::Inspect<T::AccountId>>::Balance:
From<<<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance>,
<<T as Config>::Assets as fungibles::Inspect<T::AccountId>>::Balance: From<u128>,
[u8; 32]: From<<T as frame_system::Config>::AccountId>,
{
#[pallet::call_index(0)]
#[pallet::weight(weight())]
pub fn teleport(
origin: OriginFor<T>,
params: TeleportParams<
AssetId<T>,
<<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance,
>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let dispatcher = <T as Config>::Dispatcher::default();
let asset_id = SupportedAssets::<T>::get(params.asset_id.clone())
.ok_or_else(|| Error::<T>::UnregisteredAsset)?;
let decimals = if params.asset_id == T::NativeAssetId::get() {
<T as Config>::NativeCurrency::transfer(
&who,
&Self::pallet_account(),
params.amount,
ExistenceRequirement::KeepAlive,
)?;
T::Decimals::get()
} else {
<T as Config>::Assets::transfer(
params.asset_id.clone(),
&who,
&Self::pallet_account(),
params.amount.into(),
Preservation::Protect,
)?;
<T::Assets as fungibles::metadata::Inspect<T::AccountId>>::decimals(
params.asset_id.clone(),
)
};
let to = params.recepient.0;
let from: [u8; 32] = who.clone().into();
let erc_decimals = Decimals::<T>::get(params.asset_id)
.ok_or_else(|| Error::<T>::AssetDecimalsNotFound)?;
let body = Body {
amount: {
let amount: u128 = params.amount.into();
let mut bytes = [0u8; 32];
convert_to_erc20(amount, erc_decimals, decimals).to_big_endian(&mut bytes);
alloy_primitives::U256::from_be_bytes(bytes)
},
asset_id: asset_id.0.into(),
redeem: false,
from: from.into(),
to: to.into(),
};
let dispatch_post = DispatchPost {
dest: params.destination,
from: token_gateway_id().0.to_vec(),
to: params.token_gateway,
timeout: params.timeout,
body: {
let mut encoded = vec![0];
encoded.extend_from_slice(&Body::abi_encode(&body));
encoded
},
};
let metadata = FeeMetadata { payer: who.clone(), fee: params.relayer_fee.into() };
let commitment = dispatcher
.dispatch_request(DispatchRequest::Post(dispatch_post), metadata)
.map_err(|_| Error::<T>::AssetTeleportError)?;
Self::deposit_event(Event::<T>::AssetTeleported {
from: who,
to: params.recepient,
dest: params.destination,
amount: params.amount,
commitment,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(weight())]
pub fn set_token_gateway_addresses(
origin: OriginFor<T>,
addresses: BTreeMap<StateMachine, Vec<u8>>,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
for (chain, address) in addresses {
TokenGatewayAddresses::<T>::insert(chain, address.clone());
}
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(weight())]
pub fn create_erc6160_asset(
origin: OriginFor<T>,
asset: AssetRegistration<AssetId<T>>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let VersionedHostParams::V1(SubstrateHostParams { asset_registration_fee, .. }) =
pallet_hyperbridge::Pallet::<T>::host_params();
if asset_registration_fee != Zero::zero() {
T::Currency::transfer(
&who,
&PALLET_HYPERBRIDGE.into_account_truncating(),
asset_registration_fee.into(),
Preservation::Expendable,
)?;
}
let asset_id: H256 = sp_io::hashing::keccak_256(asset.reg.symbol.as_ref()).into();
SupportedAssets::<T>::insert(asset.local_id.clone(), asset_id.clone());
LocalAssets::<T>::insert(asset_id, asset.local_id.clone());
Decimals::<T>::insert(asset.local_id, 18);
let dispatcher = <T as Config>::Dispatcher::default();
let dispatch_post = DispatchPost {
dest: T::Coprocessor::get().ok_or_else(|| Error::<T>::CoprocessorNotConfigured)?,
from: token_gateway_id().0.to_vec(),
to: token_governor_id(),
timeout: 0,
body: { RemoteERC6160AssetRegistration::CreateAsset(asset.reg).encode() },
};
let metadata = FeeMetadata { payer: who, fee: Default::default() };
let commitment = dispatcher
.dispatch_request(DispatchRequest::Post(dispatch_post), metadata)
.map_err(|_| Error::<T>::DispatchError)?;
Self::deposit_event(Event::<T>::ERC6160AssetRegistrationDispatched { commitment });
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(weight())]
pub fn update_erc6160_asset(
origin: OriginFor<T>,
asset: GatewayAssetUpdate,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let asset_id = LocalAssets::<T>::get(asset.asset_id.clone())
.ok_or_else(|| Error::<T>::UnregisteredAsset)?;
Self::ensure_admin(who.clone(), asset_id)?;
let VersionedHostParams::V1(SubstrateHostParams { asset_registration_fee, .. }) =
pallet_hyperbridge::Pallet::<T>::host_params();
if asset_registration_fee != Zero::zero() {
T::Currency::transfer(
&who,
&PALLET_HYPERBRIDGE.into_account_truncating(),
asset_registration_fee.into(),
Preservation::Expendable,
)?;
}
let dispatcher = <T as Config>::Dispatcher::default();
let dispatch_post = DispatchPost {
dest: T::Coprocessor::get().ok_or_else(|| Error::<T>::CoprocessorNotConfigured)?,
from: token_gateway_id().0.to_vec(),
to: token_governor_id(),
timeout: 0,
body: { RemoteERC6160AssetRegistration::UpdateAsset(asset).encode() },
};
let metadata = FeeMetadata { payer: who, fee: Default::default() };
let commitment = dispatcher
.dispatch_request(DispatchRequest::Post(dispatch_post), metadata)
.map_err(|_| Error::<T>::DispatchError)?;
Self::deposit_event(Event::<T>::ERC6160AssetRegistrationDispatched { commitment });
Ok(())
}
}
impl<T> Default for Pallet<T> {
fn default() -> Self {
Self(PhantomData)
}
}
}
impl<T: Config> IsmpModule for Pallet<T>
where
<T as frame_system::Config>::AccountId: From<[u8; 32]>,
<<T as Config>::NativeCurrency as Currency<T::AccountId>>::Balance: From<u128>,
<<T as Config>::Assets as fungibles::Inspect<T::AccountId>>::Balance: From<u128>,
{
fn on_accept(
&self,
PostRequest { body, from, source, dest, nonce, .. }: PostRequest,
) -> Result<(), anyhow::Error> {
if from == token_governor_id() && Some(source) == T::Coprocessor::get() {
if let Ok(metadata) = AssetMetadata::decode(&mut &body[..]) {
let asset_id: H256 = sp_io::hashing::keccak_256(metadata.symbol.as_ref()).into();
if let Some(local_asset_id) = LocalAssets::<T>::get(asset_id) {
<T::Assets as fungibles::metadata::Mutate<T::AccountId>>::set(
local_asset_id.clone(),
&T::AssetAdmin::get(),
metadata.name.to_vec(),
metadata.symbol.to_vec(),
<T::Assets as fungibles::metadata::Inspect<T::AccountId>>::decimals(
local_asset_id.clone(),
),
)
.map_err(|e| anyhow!("{e:?}"))?;
Decimals::<T>::insert(local_asset_id, metadata.decimals);
} else {
let min_balance = metadata.minimum_balance.unwrap_or(MIN_BALANCE);
let local_asset_id =
T::AssetIdFactory::create_asset_id(metadata.symbol.to_vec())?;
<T::Assets as fungibles::Create<T::AccountId>>::create(
local_asset_id.clone(),
T::AssetAdmin::get(),
true,
min_balance.into(),
)
.map_err(|e| anyhow!("{e:?}"))?;
<T::Assets as fungibles::metadata::Mutate<T::AccountId>>::set(
local_asset_id.clone(),
&T::AssetAdmin::get(),
metadata.name.to_vec(),
metadata.symbol.to_vec(),
18,
)
.map_err(|e| anyhow!("{e:?}"))?;
SupportedAssets::<T>::insert(local_asset_id.clone(), asset_id.clone());
LocalAssets::<T>::insert(asset_id, local_asset_id.clone());
Decimals::<T>::insert(local_asset_id, metadata.decimals);
}
return Ok(())
}
if let Ok(meta) = DeregisterAssets::decode(&mut &body[..]) {
for asset_id in meta.asset_ids {
if let Some(local_asset_id) = LocalAssets::<T>::get(H256::from(asset_id.0)) {
SupportedAssets::<T>::remove(local_asset_id.clone());
LocalAssets::<T>::remove(H256::from(asset_id.0));
Decimals::<T>::remove(local_asset_id.clone());
}
}
}
}
ensure!(
from == TokenGatewayAddresses::<T>::get(source).unwrap_or_default().to_vec() ||
from == token_gateway_id().0.to_vec(),
ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Unknown source contract address".to_string(),
meta: Meta { source, dest, nonce },
}
);
let body = Body::abi_decode(&mut &body[1..], true).map_err(|_| {
ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Failed to decode request body".to_string(),
meta: Meta { source, dest, nonce },
}
})?;
let local_asset_id =
LocalAssets::<T>::get(H256::from(body.asset_id.0)).ok_or_else(|| {
ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Unknown asset".to_string(),
meta: Meta { source, dest, nonce },
}
})?;
let decimals = if local_asset_id == T::NativeAssetId::get() {
T::Decimals::get()
} else {
<T::Assets as fungibles::metadata::Inspect<T::AccountId>>::decimals(
local_asset_id.clone(),
)
};
let erc_decimals = Decimals::<T>::get(local_asset_id.clone())
.ok_or_else(|| anyhow!("Asset decimals not configured"))?;
let amount = convert_to_balance(
U256::from_big_endian(&body.amount.to_be_bytes::<32>()),
erc_decimals,
decimals,
)
.map_err(|_| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Trying to withdraw Invalid amount".to_string(),
meta: Meta { source, dest, nonce },
})?;
let beneficiary: T::AccountId = body.to.0.into();
if local_asset_id == T::NativeAssetId::get() {
<T as Config>::NativeCurrency::transfer(
&Pallet::<T>::pallet_account(),
&beneficiary,
amount.into(),
ExistenceRequirement::AllowDeath,
)
.map_err(|_| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Failed to complete asset transfer".to_string(),
meta: Meta { source, dest, nonce },
})?;
} else {
<T as Config>::Assets::transfer(
local_asset_id,
&Pallet::<T>::pallet_account(),
&beneficiary,
amount.into(),
Preservation::Protect,
)
.map_err(|_| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Failed to complete asset transfer".to_string(),
meta: Meta { source, dest, nonce },
})?;
}
Self::deposit_event(Event::<T>::AssetReceived {
beneficiary,
amount: amount.into(),
source,
});
Ok(())
}
fn on_response(&self, _response: Response) -> Result<(), anyhow::Error> {
Err(anyhow!("Module does not accept responses".to_string()))
}
fn on_timeout(&self, request: Timeout) -> Result<(), anyhow::Error> {
match request {
Timeout::Request(Request::Post(PostRequest { body, source, dest, nonce, .. })) => {
let body = Body::abi_decode(&mut &body[1..], true).map_err(|_| {
ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Failed to decode request body".to_string(),
meta: Meta { source, dest, nonce },
}
})?;
let beneficiary = body.from.0.into();
let local_asset_id = LocalAssets::<T>::get(H256::from(body.asset_id.0))
.ok_or_else(|| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Unknown asset".to_string(),
meta: Meta { source, dest, nonce },
})?;
let decimals = if local_asset_id == T::NativeAssetId::get() {
T::Decimals::get()
} else {
<T::Assets as fungibles::metadata::Inspect<T::AccountId>>::decimals(
local_asset_id.clone(),
)
};
let erc_decimals = Decimals::<T>::get(local_asset_id.clone())
.ok_or_else(|| anyhow!("Asset decimals not configured"))?;
let amount = convert_to_balance(
U256::from_big_endian(&body.amount.to_be_bytes::<32>()),
erc_decimals,
decimals,
)
.map_err(|_| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Trying to withdraw Invalid amount".to_string(),
meta: Meta { source, dest, nonce },
})?;
if local_asset_id == T::NativeAssetId::get() {
<T as Config>::NativeCurrency::transfer(
&Pallet::<T>::pallet_account(),
&beneficiary,
amount.into(),
ExistenceRequirement::AllowDeath,
)
.map_err(|_| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Failed to complete asset transfer".to_string(),
meta: Meta { source, dest, nonce },
})?;
} else {
<T as Config>::Assets::transfer(
local_asset_id,
&Pallet::<T>::pallet_account(),
&beneficiary,
amount.into(),
Preservation::Protect,
)
.map_err(|_| ismp::error::Error::ModuleDispatchError {
msg: "Token Gateway: Failed to complete asset transfer".to_string(),
meta: Meta { source, dest, nonce },
})?;
}
Pallet::<T>::deposit_event(Event::<T>::AssetRefunded {
beneficiary,
amount: amount.into(),
source: dest,
});
},
Timeout::Request(Request::Get(get)) => Err(ismp::error::Error::ModuleDispatchError {
msg: "Tried to timeout unsupported request type".to_string(),
meta: Meta { source: get.source, dest: get.dest, nonce: get.nonce },
})?,
Timeout::Response(response) => Err(ismp::error::Error::ModuleDispatchError {
msg: "Tried to timeout unsupported request type".to_string(),
meta: Meta {
source: response.source_chain(),
dest: response.dest_chain(),
nonce: response.nonce(),
},
})?,
}
Ok(())
}
}
fn weight() -> Weight {
Weight::from_parts(300_000_000, 0)
}
impl<T: Config> Pallet<T> {
pub fn ensure_admin(who: T::AccountId, asset_id: AssetId<T>) -> Result<(), Error<T>> {
let owner = <T::Assets as fungibles::roles::Inspect<T::AccountId>>::admin(asset_id)
.ok_or_else(|| Error::<T>::UnknownAsset)?;
ensure!(who == owner, Error::<T>::NotAssetOwner);
Ok(())
}
}