ISMP Modules
This section will provide an in depth guide on how to build an ismp module. Usually in substrate an ismp module is going to be a pallet.
Defining a Module Id
Ismp uses a unique module id to identify ismp modules and route messages correctly, in contract environments this module id is the contract address. For a substrate pallet, you will need to define an 8 byte module id that is unique to the pallet
use pallet_ismp::ModuleId;
pub const EXAMPLE_MODULE_ID: ModuleId = ModuleId::Pallet(PalletId(*b"EXPL-MOD"));
Building the pallet
In this section we build an example pallet that allows us dispatch and receive messages from hyperbridge.
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{pallet_prelude::*, PalletId};
use pallet_ismp::ModuleId;
use ismp::host::ethereum;
pub const EXAMPLE_MODULE_ID: ModuleId = ModuleId::Pallet(PalletId(*b"EXPL-MOD"));
#[pallet::config]
pub trait Config: frame_system::Config + pallet_ismp::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// [`IsmpDispatcher`] implementation
type IsmpDispatcher: IsmpDispatcher<Account = Self::AccountId, Balance = Self::Balance> + Default;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
MessageReceived,
TimeoutProcessed
}
// Errors encountered
#[pallet::error]
pub enum Error<T> {
MessageDispatchFailed
}
// Hack for implementing the [`Default`] bound needed for
// [`IsmpDispatcher`](ismp::dispatcher::IsmpDispatcher) and
// [`IsmpModule`](ismp::module::IsmpModule)
impl<T> Default for Pallet<T> {
fn default() -> Self {
Self(PhantomData)
}
}
/// Extrisnic params for evm dispatch
#[derive(
Clone, codec::Encode, codec::Decode, scale_info::TypeInfo, PartialEq, Eq, RuntimeDebug,
)]
pub struct Params<Balance> {
/// Destination contract
pub module: sp_core::H160,
/// Destination EVM host
pub destination: Ethereum,
/// Timeout timestamp on destination chain in seconds
pub timeout: u64
/// A relayer fee for message delivery
pub fee: Balance
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Dispatch request to a connected EVM chain.
#[pallet::weight(Weight::from_parts(1_000_000, 0))]
#[pallet::call_index(1)]
pub fn dispatch_to_evm(origin: OriginFor<T>, params: Params<T::Balance>) -> DispatchResult {
let origin = ensure_signed(origin)?;
let post = DispatchPost {
dest: StateMachine::Ethereum(params.destination),
from: EXAMPLE_MODULE_ID.to_bytes(),
to: params.module.0.to_vec(),
timeout: params.timeout,
body: b"Hello from polkadot".to_vec(),
};
let dispatcher = T::IsmpDispatcher::default();
// dispatch the request
// This call will attempt to collect the protocol fee and relayer fee from the user's account
dispatcher
.dispatch_request(
DispatchRequest::Post(post.clone()),
FeeMetadata { payer: origin.clone(), fee: params.fee },
)
.map_err(|_| Error::<T>::MessageDispatchFailed)?;
Ok(())
}
}
}
impl<T> IsmpModule for Pallet<T> {
fn on_accept(&self, request: Post) -> Result<(), ismp::Error> {
// Here you would perform validations on the post request data
// Ensure it can be executed successfully before making any state changes
// You can also dispatch a post response after execution
Self::deposit_event(Event::<T>::MessageReceived);
Ok(())
}
fn on_response(&self, _response: Response) -> Result<(), ismp::Error> {
// Here you would perform validations on the post request data
// Ensure it can be executed successfully before making any state changes
Self::deposit_event(Event::<T>::MessageReceived);
Ok(())
}
fn on_timeout(&self, _request: Timeout) -> Result<(), ismp::Error> {
// Here you would revert all the state changes that were made when the
// request was initially dispatched
Self::deposit_event(Event::<T>::TimeoutProcessed);
Ok(())
}
}
Runtime Integration
Now the pallet needs to be added to the runtime and also the IsmpRouter
.
impl pallet_hyperbridge::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
// pallet-ismp implements the IsmpHost
type IsmpHost = Ismp;
}
impl pallet_example::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
// Set pallet hyperbridge as the dispatcher
type IsmpDispatcher = Hyperbridge;
}
// ... omitted impl blocks
construct_runtime! {
// ...
Ismp: pallet_ismp,
Hyperbridge: pallet_hyperbridge,
Example: pallet_example
}
#[derive(Default)]
struct ModuleRouter;
impl IsmpRouter for ModuleRouter {
fn module_for_id(&self, id: Vec<u8>) -> Result<Box<dyn IsmpModule>, Error> {
return match id.as_slice() {
PALLET_HYPERBRIDGE_ID => Ok(Box::new(pallet_hyperbridge::Pallet::<Runtime>::default())),
id if id == EXAMPLE_MODULE_ID.to_bytes().as_slice() => Ok(Box::new(pallet_ismp::pallet_example::Pallet::<Runtime>::default()))
// ... other modules
_ => Err(Error::ModuleNotFound(id)),
};
}
}
Security Considerations
- Irreversible Changes:
The IsmpHost
doesn't store receipts for failed messages, ensure irreversible state changes occur only after a message effectively meets all success criteria.