pallet_hyperbridge/
lib.rs

1// Copyright (C) Polytope Labs Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! # Pallet Hyperbridge
17//!
18//! Pallet hyperbridge mediates the connection between hyperbridge and substrate-based chains. This
19//! pallet provides:
20//!
21//!  - An [`IsmpDispatcher`] implementation which collects hyperbridge's protocol fees and commits
22//!    the reciepts for these fees to child storage. Hyperbridge will only accept messages that have
23//!    been paid for using this module.
24//!  - An [`IsmpModule`] which recieves and processes requests from hyperbridge. These requests are
25//!    dispatched by hyperbridge governance and may adjust fees or request payouts for both relayers
26//!    and protocol revenue.
27//!
28//! This pallet contains no calls and dispatches no requests. Substrate based chains should use this
29//! to dispatch requests that should be processed by hyperbridge.
30//!
31//! ## Usage
32//!
33//! This module must be configured as an [`IsmpModule`] in your
34//! [`IsmpRouter`](ismp::router::IsmpRouter) implementation so that it may receive important
35//! messages from hyperbridge such as paramter updates or relayer fee withdrawals.
36//!
37//! ```rust,ignore
38//! use ismp::module::IsmpModule;
39//! use ismp::router::IsmpRouter;
40//!
41//! #[derive(Default)]
42//! struct ModuleRouter;
43//!
44//! impl IsmpRouter for ModuleRouter {
45//!     fn module_for_id(&self, id: Vec<u8>) -> Result<Box<dyn IsmpModule>, anyhow::Error> {
46//!         return match id.as_slice() {
47//!             pallet_hyperbridge::PALLET_HYPERBRIDGE_ID => Ok(Box::new(pallet_hyperbridge::Pallet::<Runtime>::default())),
48//!             _ => Err(Error::ModuleNotFound(id)),
49//!         };
50//!     }
51//! }
52//! ```
53
54#![cfg_attr(not(feature = "std"), no_std)]
55#![deny(missing_docs)]
56
57extern crate alloc;
58
59use alloc::{collections::BTreeMap, format};
60use codec::{Decode, DecodeWithMemTracking, Encode};
61use frame_support::{
62	sp_runtime::traits::AccountIdConversion,
63	traits::{fungible::Mutate, tokens::Preservation, Get},
64};
65use ismp::{
66	dispatcher::{DispatchRequest, FeeMetadata, IsmpDispatcher},
67	host::StateMachine,
68	module::IsmpModule,
69	router::{PostRequest, PostResponse, Response, Timeout},
70};
71pub use pallet::*;
72use pallet_ismp::RELAYER_FEE_ACCOUNT;
73use polkadot_sdk::*;
74use primitive_types::H256;
75
76pub mod child_trie;
77
78/// Host params for substrate based chains
79#[derive(
80	Debug,
81	Clone,
82	Encode,
83	Decode,
84	DecodeWithMemTracking,
85	scale_info::TypeInfo,
86	PartialEq,
87	Eq,
88	Default,
89)]
90pub struct SubstrateHostParams<B> {
91	/// The default per byte fee
92	pub default_per_byte_fee: B,
93	/// Per byte fee configured for specific chains
94	pub per_byte_fees: BTreeMap<StateMachine, B>,
95	/// Asset registration fee
96	pub asset_registration_fee: B,
97}
98
99/// Parameters that govern the working operations of this module. Versioned for ease of migration.
100#[derive(
101	Debug, Clone, Encode, Decode, DecodeWithMemTracking, scale_info::TypeInfo, PartialEq, Eq,
102)]
103pub enum VersionedHostParams<Balance> {
104	/// The per-byte fee that hyperbridge charges for outgoing requests and responses.
105	V1(SubstrateHostParams<Balance>),
106}
107
108impl<Balance: Default> Default for VersionedHostParams<Balance> {
109	fn default() -> Self {
110		VersionedHostParams::V1(Default::default())
111	}
112}
113
114#[frame_support::pallet]
115pub mod pallet {
116	use super::*;
117	use frame_support::{pallet_prelude::*, PalletId};
118
119	/// [`IsmpModule`] module identifier for incoming requests from hyperbridge
120	pub const PALLET_HYPERBRIDGE_ID: &'static [u8] = b"HYPR-FEE";
121
122	/// [`PalletId`] where protocol fees will be collected
123	pub const PALLET_HYPERBRIDGE: PalletId = PalletId(*b"HYPR-FEE");
124
125	#[pallet::config]
126	pub trait Config: polkadot_sdk::frame_system::Config + pallet_ismp::Config {
127		/// Because this pallet emits events, it depends on the runtime's definition of an event.
128		type RuntimeEvent: From<Event<Self>>
129			+ IsType<<Self as polkadot_sdk::frame_system::Config>::RuntimeEvent>;
130
131		/// The underlying [`IsmpHost`] implementation
132		type IsmpHost: IsmpDispatcher<Account = Self::AccountId, Balance = Self::Balance> + Default;
133	}
134
135	#[pallet::pallet]
136	#[pallet::without_storage_info]
137	pub struct Pallet<T>(_);
138
139	/// The host parameters of the pallet-hyperbridge.
140	#[pallet::storage]
141	#[pallet::getter(fn host_params)]
142	pub type HostParams<T> =
143		StorageValue<_, VersionedHostParams<<T as pallet_ismp::Config>::Balance>, ValueQuery>;
144
145	#[pallet::event]
146	#[pallet::generate_deposit(pub(super) fn deposit_event)]
147	pub enum Event<T: Config> {
148		/// Hyperbridge governance has now updated it's host params on this chain.
149		HostParamsUpdated {
150			/// The old host params
151			old: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
152			/// The new host params
153			new: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
154		},
155		/// A relayer has withdrawn some fees
156		RelayerFeeWithdrawn {
157			/// The amount that was withdrawn
158			amount: <T as pallet_ismp::Config>::Balance,
159			/// The withdrawal beneficiary
160			account: T::AccountId,
161		},
162		/// Hyperbridge has withdrawn it's protocol revenue
163		ProtocolRevenueWithdrawn {
164			/// The amount that was withdrawn
165			amount: <T as pallet_ismp::Config>::Balance,
166			/// The withdrawal beneficiary
167			account: T::AccountId,
168		},
169	}
170
171	// Errors encountered by pallet-hyperbridge
172	#[pallet::error]
173	pub enum Error<T> {}
174
175	// Hack for implementing the [`Default`] bound needed for
176	// [`IsmpDispatcher`](ismp::dispatcher::IsmpDispatcher) and
177	// [`IsmpModule`](ismp::module::IsmpModule)
178	impl<T> Default for Pallet<T> {
179		fn default() -> Self {
180			Self(PhantomData)
181		}
182	}
183}
184
185/// [`IsmpDispatcher`] implementation for dispatching requests to the hyperbridge coprocessor.
186/// Charges the hyperbridge protocol fee on a per-byte basis.
187///
188/// **NOTE** Hyperbridge WILL NOT accept requests that were not dispatched through this
189/// implementation.
190impl<T> IsmpDispatcher for Pallet<T>
191where
192	T: Config,
193	T::Balance: Into<u128> + From<u128>,
194{
195	type Account = T::AccountId;
196	type Balance = T::Balance;
197
198	fn dispatch_request(
199		&self,
200		request: DispatchRequest,
201		fee: FeeMetadata<Self::Account, Self::Balance>,
202	) -> Result<H256, anyhow::Error> {
203		let fees = match request {
204			DispatchRequest::Post(ref post) => {
205				let VersionedHostParams::V1(params) = Self::host_params();
206				let per_byte_fee: u128 =
207					(*params.per_byte_fees.get(&post.dest).unwrap_or(&params.default_per_byte_fee))
208						.into();
209				// minimum fee is 32 bytes
210				let fees = if post.body.len() < 32 {
211					per_byte_fee * 32u128
212				} else {
213					per_byte_fee * post.body.len() as u128
214				};
215
216				// collect protocol fees
217				if fees != 0 {
218					T::Currency::transfer(
219						&fee.payer,
220						&PALLET_HYPERBRIDGE.into_account_truncating(),
221						fees.into(),
222						Preservation::Expendable,
223					)
224					.map_err(|err| {
225						ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
226					})?;
227				}
228
229				fees
230			},
231			DispatchRequest::Get(_) => Default::default(),
232		};
233
234		let host = <T as Config>::IsmpHost::default();
235		let commitment = host.dispatch_request(request, fee)?;
236
237		// commit the fee collected to child-trie
238		child_trie::RequestPayments::insert(commitment, fees);
239
240		Ok(commitment)
241	}
242
243	fn dispatch_response(
244		&self,
245		response: PostResponse,
246		fee: FeeMetadata<Self::Account, Self::Balance>,
247	) -> Result<H256, anyhow::Error> {
248		// collect protocol fees
249		let VersionedHostParams::V1(params) = Self::host_params();
250		let per_byte_fee: u128 = (*params
251			.per_byte_fees
252			.get(&response.dest_chain())
253			.unwrap_or(&params.default_per_byte_fee))
254		.into();
255		// minimum fee is 32 bytes
256		let fees = if response.response.len() < 32 {
257			per_byte_fee * 32u128
258		} else {
259			per_byte_fee * response.response.len() as u128
260		};
261
262		if fees != 0 {
263			T::Currency::transfer(
264				&fee.payer,
265				&PALLET_HYPERBRIDGE.into_account_truncating(),
266				fees.into(),
267				Preservation::Expendable,
268			)
269			.map_err(|err| {
270				ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
271			})?;
272		}
273
274		let host = <T as Config>::IsmpHost::default();
275		let commitment = host.dispatch_response(response, fee)?;
276
277		// commit the collected to child-trie
278		child_trie::ResponsePayments::insert(commitment, fees);
279
280		Ok(commitment)
281	}
282}
283
284/// A request to withdraw some funds. Could either be for protocol revenue or relayer fees.
285#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
286pub struct WithdrawalRequest<Account, Amount> {
287	/// The amount to be withdrawn
288	pub amount: Amount,
289	/// The withdrawal beneficiary
290	pub account: Account,
291}
292
293/// Cross-chain messages to this module. This module will only accept messages from the hyperbridge
294/// chain. Assumed to be configured in [`pallet_ismp::Config`]
295#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
296pub enum Message<Account, Balance> {
297	/// Set some new host params
298	UpdateHostParams(VersionedHostParams<Balance>),
299	/// Withdraw the hyperbridge protocol reveneue
300	WithdrawProtocolFees(WithdrawalRequest<Account, Balance>),
301	/// Withdraw the fees owed to a relayer
302	WithdrawRelayerFees(WithdrawalRequest<Account, Balance>),
303}
304
305impl<T> IsmpModule for Pallet<T>
306where
307	T: Config,
308	T::Balance: Into<u128> + From<u128>,
309{
310	fn on_accept(&self, request: PostRequest) -> Result<(), anyhow::Error> {
311		// this of course assumes that hyperbridge is configured as the coprocessor.
312		let source = request.source;
313		if Some(source) != T::Coprocessor::get() {
314			Err(ismp::Error::Custom(format!("Invalid request source: {source}")))?
315		}
316
317		let message =
318			Message::<T::AccountId, T::Balance>::decode(&mut &request.body[..]).map_err(|err| {
319				ismp::Error::Custom(format!("Failed to decode per-byte fee: {err:?}"))
320			})?;
321
322		match message {
323			Message::UpdateHostParams(new) => {
324				let old = HostParams::<T>::get();
325				HostParams::<T>::put(new.clone());
326				Self::deposit_event(Event::<T>::HostParamsUpdated { old, new });
327			},
328			Message::WithdrawProtocolFees(WithdrawalRequest { account, amount }) => {
329				T::Currency::transfer(
330					&PALLET_HYPERBRIDGE.into_account_truncating(),
331					&account,
332					amount,
333					Preservation::Expendable,
334				)
335				.map_err(|err| {
336					ismp::Error::Custom(format!("Error withdrawing protocol fees: {err:?}"))
337				})?;
338
339				Self::deposit_event(Event::<T>::ProtocolRevenueWithdrawn { account, amount })
340			},
341			Message::WithdrawRelayerFees(WithdrawalRequest { account, amount }) => {
342				T::Currency::transfer(
343					&RELAYER_FEE_ACCOUNT.into_account_truncating(),
344					&account,
345					amount,
346					Preservation::Expendable,
347				)
348				.map_err(|err| {
349					ismp::Error::Custom(format!("Error withdrawing protocol fees: {err:?}"))
350				})?;
351
352				Self::deposit_event(Event::<T>::RelayerFeeWithdrawn { account, amount })
353			},
354		};
355
356		Ok(())
357	}
358
359	fn on_response(&self, _response: Response) -> Result<(), anyhow::Error> {
360		// this module does not expect responses
361		Err(ismp::Error::CannotHandleMessage.into())
362	}
363
364	fn on_timeout(&self, _request: Timeout) -> Result<(), anyhow::Error> {
365		// this module does not dispatch requests
366		Err(ismp::Error::CannotHandleMessage.into())
367	}
368}