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::{sp_runtime::Weight, *};
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		/// The underlying [`IsmpHost`] implementation
128		type IsmpHost: IsmpDispatcher<Account = Self::AccountId, Balance = Self::Balance> + Default;
129	}
130
131	#[pallet::pallet]
132	#[pallet::without_storage_info]
133	pub struct Pallet<T>(_);
134
135	/// The host parameters of the pallet-hyperbridge.
136	#[pallet::storage]
137	#[pallet::getter(fn host_params)]
138	pub type HostParams<T> =
139		StorageValue<_, VersionedHostParams<<T as pallet_ismp::Config>::Balance>, ValueQuery>;
140
141	#[pallet::event]
142	#[pallet::generate_deposit(pub(super) fn deposit_event)]
143	pub enum Event<T: Config> {
144		/// Hyperbridge governance has now updated it's host params on this chain.
145		HostParamsUpdated {
146			/// The old host params
147			old: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
148			/// The new host params
149			new: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
150		},
151		/// A relayer has withdrawn some fees
152		RelayerFeeWithdrawn {
153			/// The amount that was withdrawn
154			amount: <T as pallet_ismp::Config>::Balance,
155			/// The withdrawal beneficiary
156			account: T::AccountId,
157		},
158	}
159
160	// Errors encountered by pallet-hyperbridge
161	#[pallet::error]
162	pub enum Error<T> {}
163
164	// Hack for implementing the [`Default`] bound needed for
165	// [`IsmpDispatcher`](ismp::dispatcher::IsmpDispatcher) and
166	// [`IsmpModule`](ismp::module::IsmpModule)
167	impl<T> Default for Pallet<T> {
168		fn default() -> Self {
169			Self(PhantomData)
170		}
171	}
172}
173
174/// [`IsmpDispatcher`] implementation for dispatching requests to the hyperbridge coprocessor.
175/// Charges the hyperbridge protocol fee on a per-byte basis.
176///
177/// **NOTE** Hyperbridge WILL NOT accept requests that were not dispatched through this
178/// implementation.
179impl<T> IsmpDispatcher for Pallet<T>
180where
181	T: Config,
182	T::Balance: Into<u128> + From<u128>,
183{
184	type Account = T::AccountId;
185	type Balance = T::Balance;
186
187	fn dispatch_request(
188		&self,
189		request: DispatchRequest,
190		fee: FeeMetadata<Self::Account, Self::Balance>,
191	) -> Result<H256, anyhow::Error> {
192		let fees = match request {
193			DispatchRequest::Post(ref post) => {
194				let VersionedHostParams::V1(params) = Self::host_params();
195				let per_byte_fee: u128 =
196					(*params.per_byte_fees.get(&post.dest).unwrap_or(&params.default_per_byte_fee))
197						.into();
198				// minimum fee is 32 bytes
199				let fees = if post.body.len() < 32 {
200					per_byte_fee * 32u128
201				} else {
202					per_byte_fee * post.body.len() as u128
203				};
204
205				// collect protocol fees
206				if fees != 0 {
207					T::Currency::transfer(
208						&fee.payer,
209						&RELAYER_FEE_ACCOUNT.into_account_truncating(),
210						fees.into(),
211						Preservation::Expendable,
212					)
213					.map_err(|err| {
214						ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
215					})?;
216				}
217
218				fees
219			},
220			DispatchRequest::Get(_) => Default::default(),
221		};
222
223		let host = <T as Config>::IsmpHost::default();
224		let commitment = host.dispatch_request(request, fee)?;
225
226		// commit the fee collected to child-trie
227		child_trie::RequestPayments::insert(commitment, fees);
228
229		Ok(commitment)
230	}
231
232	fn dispatch_response(
233		&self,
234		response: PostResponse,
235		fee: FeeMetadata<Self::Account, Self::Balance>,
236	) -> Result<H256, anyhow::Error> {
237		// collect protocol fees
238		let VersionedHostParams::V1(params) = Self::host_params();
239		let per_byte_fee: u128 = (*params
240			.per_byte_fees
241			.get(&response.dest_chain())
242			.unwrap_or(&params.default_per_byte_fee))
243		.into();
244		// minimum fee is 32 bytes
245		let fees = if response.response.len() < 32 {
246			per_byte_fee * 32u128
247		} else {
248			per_byte_fee * response.response.len() as u128
249		};
250
251		if fees != 0 {
252			T::Currency::transfer(
253				&fee.payer,
254				&RELAYER_FEE_ACCOUNT.into_account_truncating(),
255				fees.into(),
256				Preservation::Expendable,
257			)
258			.map_err(|err| {
259				ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
260			})?;
261		}
262
263		let host = <T as Config>::IsmpHost::default();
264		let commitment = host.dispatch_response(response, fee)?;
265
266		// commit the collected to child-trie
267		child_trie::ResponsePayments::insert(commitment, fees);
268
269		Ok(commitment)
270	}
271}
272
273/// A request to withdraw some funds. Could either be for protocol revenue or relayer fees.
274#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
275pub struct WithdrawalRequest<Account, Amount> {
276	/// The amount to be withdrawn
277	pub amount: Amount,
278	/// The withdrawal beneficiary
279	pub account: Account,
280}
281
282/// Cross-chain messages to this module. This module will only accept messages from the hyperbridge
283/// chain. Assumed to be configured in [`pallet_ismp::Config`]
284#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
285pub enum Message<Account, Balance> {
286	/// Set some new host params
287	#[codec(index = 0)]
288	UpdateHostParams(VersionedHostParams<Balance>),
289	/// Withdraw the fees owed to a relayer
290	#[codec(index = 2)]
291	WithdrawRelayerFees(WithdrawalRequest<Account, Balance>),
292}
293
294impl<T> IsmpModule for Pallet<T>
295where
296	T: Config,
297	T::Balance: Into<u128> + From<u128>,
298{
299	fn on_accept(&self, request: PostRequest) -> Result<Weight, anyhow::Error> {
300		// this of course assumes that hyperbridge is configured as the coprocessor.
301		let source = request.source;
302		if Some(source) != T::Coprocessor::get() {
303			Err(ismp::Error::Custom(format!("Invalid request source: {source}")))?
304		}
305
306		let message =
307			Message::<T::AccountId, T::Balance>::decode(&mut &request.body[..]).map_err(|err| {
308				ismp::Error::Custom(format!("Failed to decode per-byte fee: {err:?}"))
309			})?;
310
311		let weight = match message {
312			Message::UpdateHostParams(new) => {
313				let old = HostParams::<T>::get();
314				HostParams::<T>::put(new.clone());
315				Self::deposit_event(Event::<T>::HostParamsUpdated { old, new });
316				T::DbWeight::get().reads_writes(0, 0)
317			},
318			Message::WithdrawRelayerFees(WithdrawalRequest { account, amount }) => {
319				T::Currency::transfer(
320					&RELAYER_FEE_ACCOUNT.into_account_truncating(),
321					&account,
322					amount,
323					Preservation::Expendable,
324				)
325				.map_err(|err| {
326					ismp::Error::Custom(format!("Error withdrawing protocol fees: {err:?}"))
327				})?;
328
329				Self::deposit_event(Event::<T>::RelayerFeeWithdrawn { account, amount });
330				T::DbWeight::get().reads_writes(0, 0)
331			},
332		};
333
334		Ok(weight)
335	}
336
337	fn on_response(&self, _response: Response) -> Result<Weight, anyhow::Error> {
338		// this module does not expect responses
339		Err(ismp::Error::CannotHandleMessage.into())
340	}
341
342	fn on_timeout(&self, _request: Timeout) -> Result<Weight, anyhow::Error> {
343		// this module does not dispatch requests
344		Err(ismp::Error::CannotHandleMessage.into())
345	}
346}