Skip to content

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.

Sample Implementation