#![cfg_attr(not(feature = "std"), no_std)]
#![deny(missing_docs)]
extern crate alloc;
use alloc::{collections::BTreeMap, format};
use codec::{Decode, Encode};
use frame_support::{
sp_runtime::traits::AccountIdConversion,
traits::{fungible::Mutate, tokens::Preservation, Get},
};
use ismp::{
dispatcher::{DispatchRequest, FeeMetadata, IsmpDispatcher},
host::StateMachine,
module::IsmpModule,
router::{PostRequest, PostResponse, Response, Timeout},
};
pub use pallet::*;
use pallet_ismp::RELAYER_FEE_ACCOUNT;
use primitive_types::H256;
pub mod child_trie;
#[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq, Default)]
pub struct SubstrateHostParams<B> {
pub default_per_byte_fee: B,
pub per_byte_fees: BTreeMap<StateMachine, B>,
pub asset_registration_fee: B,
}
#[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq)]
pub enum VersionedHostParams<Balance> {
V1(SubstrateHostParams<Balance>),
}
impl<Balance: Default> Default for VersionedHostParams<Balance> {
fn default() -> Self {
VersionedHostParams::V1(Default::default())
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{pallet_prelude::*, PalletId};
pub const PALLET_HYPERBRIDGE_ID: &'static [u8] = b"HYPR-FEE";
pub const PALLET_HYPERBRIDGE: PalletId = PalletId(*b"HYPR-FEE");
#[pallet::config]
pub trait Config: frame_system::Config + pallet_ismp::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type IsmpHost: IsmpDispatcher<Account = Self::AccountId, Balance = Self::Balance> + Default;
}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn host_params)]
pub type HostParams<T> =
StorageValue<_, VersionedHostParams<<T as pallet_ismp::Config>::Balance>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
HostParamsUpdated {
old: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
new: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
},
RelayerFeeWithdrawn {
amount: <T as pallet_ismp::Config>::Balance,
account: T::AccountId,
},
ProtocolRevenueWithdrawn {
amount: <T as pallet_ismp::Config>::Balance,
account: T::AccountId,
},
}
#[pallet::error]
pub enum Error<T> {}
impl<T> Default for Pallet<T> {
fn default() -> Self {
Self(PhantomData)
}
}
}
impl<T> IsmpDispatcher for Pallet<T>
where
T: Config,
T::Balance: Into<u128> + From<u128>,
{
type Account = T::AccountId;
type Balance = T::Balance;
fn dispatch_request(
&self,
request: DispatchRequest,
fee: FeeMetadata<Self::Account, Self::Balance>,
) -> Result<H256, anyhow::Error> {
let fees = match request {
DispatchRequest::Post(ref post) => {
let VersionedHostParams::V1(params) = Self::host_params();
let per_byte_fee: u128 =
(*params.per_byte_fees.get(&post.dest).unwrap_or(¶ms.default_per_byte_fee))
.into();
let fees = if post.body.len() < 32 {
per_byte_fee * 32u128
} else {
per_byte_fee * post.body.len() as u128
};
if fees != 0 {
T::Currency::transfer(
&fee.payer,
&PALLET_HYPERBRIDGE.into_account_truncating(),
fees.into(),
Preservation::Expendable,
)
.map_err(|err| {
ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
})?;
}
fees
},
DispatchRequest::Get(_) => Default::default(),
};
let host = <T as Config>::IsmpHost::default();
let commitment = host.dispatch_request(request, fee)?;
child_trie::RequestPayments::insert(commitment, fees);
Ok(commitment)
}
fn dispatch_response(
&self,
response: PostResponse,
fee: FeeMetadata<Self::Account, Self::Balance>,
) -> Result<H256, anyhow::Error> {
let VersionedHostParams::V1(params) = Self::host_params();
let per_byte_fee: u128 = (*params
.per_byte_fees
.get(&response.dest_chain())
.unwrap_or(¶ms.default_per_byte_fee))
.into();
let fees = if response.response.len() < 32 {
per_byte_fee * 32u128
} else {
per_byte_fee * response.response.len() as u128
};
if fees != 0 {
T::Currency::transfer(
&fee.payer,
&PALLET_HYPERBRIDGE.into_account_truncating(),
fees.into(),
Preservation::Expendable,
)
.map_err(|err| {
ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
})?;
}
let host = <T as Config>::IsmpHost::default();
let commitment = host.dispatch_response(response, fee)?;
child_trie::ResponsePayments::insert(commitment, fees);
Ok(commitment)
}
}
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct WithdrawalRequest<Account, Amount> {
pub amount: Amount,
pub account: Account,
}
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum Message<Account, Balance> {
UpdateHostParams(VersionedHostParams<Balance>),
WithdrawProtocolFees(WithdrawalRequest<Account, Balance>),
WithdrawRelayerFees(WithdrawalRequest<Account, Balance>),
}
impl<T> IsmpModule for Pallet<T>
where
T: Config,
T::Balance: Into<u128> + From<u128>,
{
fn on_accept(&self, request: PostRequest) -> Result<(), anyhow::Error> {
let source = request.source;
if Some(source) != T::Coprocessor::get() {
Err(ismp::Error::Custom(format!("Invalid request source: {source}")))?
}
let message =
Message::<T::AccountId, T::Balance>::decode(&mut &request.body[..]).map_err(|err| {
ismp::Error::Custom(format!("Failed to decode per-byte fee: {err:?}"))
})?;
match message {
Message::UpdateHostParams(new) => {
let old = HostParams::<T>::get();
HostParams::<T>::put(new.clone());
Self::deposit_event(Event::<T>::HostParamsUpdated { old, new });
},
Message::WithdrawProtocolFees(WithdrawalRequest { account, amount }) => {
T::Currency::transfer(
&PALLET_HYPERBRIDGE.into_account_truncating(),
&account,
amount,
Preservation::Expendable,
)
.map_err(|err| {
ismp::Error::Custom(format!("Error withdrawing protocol fees: {err:?}"))
})?;
Self::deposit_event(Event::<T>::ProtocolRevenueWithdrawn { account, amount })
},
Message::WithdrawRelayerFees(WithdrawalRequest { account, amount }) => {
T::Currency::transfer(
&RELAYER_FEE_ACCOUNT.into_account_truncating(),
&account,
amount,
Preservation::Expendable,
)
.map_err(|err| {
ismp::Error::Custom(format!("Error withdrawing protocol fees: {err:?}"))
})?;
Self::deposit_event(Event::<T>::RelayerFeeWithdrawn { account, amount })
},
};
Ok(())
}
fn on_response(&self, _response: Response) -> Result<(), anyhow::Error> {
Err(ismp::Error::CannotHandleMessage.into())
}
fn on_timeout(&self, _request: Timeout) -> Result<(), anyhow::Error> {
Err(ismp::Error::CannotHandleMessage.into())
}
}