1#![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#[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 pub default_per_byte_fee: B,
93 pub per_byte_fees: BTreeMap<StateMachine, B>,
95 pub asset_registration_fee: B,
97}
98
99#[derive(
101 Debug, Clone, Encode, Decode, DecodeWithMemTracking, scale_info::TypeInfo, PartialEq, Eq,
102)]
103pub enum VersionedHostParams<Balance> {
104 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 pub const PALLET_HYPERBRIDGE_ID: &'static [u8] = b"HYPR-FEE";
121
122 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 type RuntimeEvent: From<Event<Self>>
129 + IsType<<Self as polkadot_sdk::frame_system::Config>::RuntimeEvent>;
130
131 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 #[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 HostParamsUpdated {
150 old: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
152 new: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
154 },
155 RelayerFeeWithdrawn {
157 amount: <T as pallet_ismp::Config>::Balance,
159 account: T::AccountId,
161 },
162 ProtocolRevenueWithdrawn {
164 amount: <T as pallet_ismp::Config>::Balance,
166 account: T::AccountId,
168 },
169 }
170
171 #[pallet::error]
173 pub enum Error<T> {}
174
175 impl<T> Default for Pallet<T> {
179 fn default() -> Self {
180 Self(PhantomData)
181 }
182 }
183}
184
185impl<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(¶ms.default_per_byte_fee))
208 .into();
209 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 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 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 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(¶ms.default_per_byte_fee))
254 .into();
255 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 child_trie::ResponsePayments::insert(commitment, fees);
279
280 Ok(commitment)
281 }
282}
283
284#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
286pub struct WithdrawalRequest<Account, Amount> {
287 pub amount: Amount,
289 pub account: Account,
291}
292
293#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
296pub enum Message<Account, Balance> {
297 UpdateHostParams(VersionedHostParams<Balance>),
299 WithdrawProtocolFees(WithdrawalRequest<Account, Balance>),
301 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 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 Err(ismp::Error::CannotHandleMessage.into())
362 }
363
364 fn on_timeout(&self, _request: Timeout) -> Result<(), anyhow::Error> {
365 Err(ismp::Error::CannotHandleMessage.into())
367 }
368}