#![cfg_attr(not(feature = "std"), no_std)]
mod envelope;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod test;
use codec::{Decode, DecodeAll, Encode};
use envelope::Envelope;
use frame_support::{
traits::{
fungible::{Inspect, Mutate},
tokens::{Fortitude, Preservation},
},
weights::WeightToFee,
PalletError,
};
use frame_system::ensure_signed;
use scale_info::TypeInfo;
use sp_core::H160;
use sp_runtime::traits::Zero;
use sp_std::vec;
use xcm::prelude::{
send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm, XcmContext, XcmHash,
};
use xcm_executor::traits::TransactAsset;
use snowbridge_core::{
inbound::{Message, VerificationError, Verifier},
sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters,
StaticLookup,
};
use snowbridge_router_primitives::inbound::{
ConvertMessage, ConvertMessageError, VersionedMessage,
};
use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError};
pub use weights::WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
use snowbridge_beacon_primitives::BeaconHeader;
type BalanceOf<T> =
<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
pub use pallet::*;
pub const LOG_TARGET: &str = "snowbridge-inbound-queue";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use sp_core::H256;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<T> {
fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256);
}
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Verifier: Verifier;
type Token: Mutate<Self::AccountId> + Inspect<Self::AccountId>;
type XcmSender: SendXcm;
#[pallet::constant]
type GatewayAddress: Get<H160>;
type MessageConverter: ConvertMessage<
AccountId = Self::AccountId,
Balance = BalanceOf<Self>,
>;
type ChannelLookup: StaticLookup<Source = ChannelId, Target = Channel>;
type PricingParameters: Get<PricingParameters<BalanceOf<Self>>>;
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type Helper: BenchmarkHelper<Self>;
type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
type LengthToFee: WeightToFee<Balance = BalanceOf<Self>>;
type MaxMessageSize: Get<u32>;
type AssetTransactor: TransactAsset;
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
MessageReceived {
channel_id: ChannelId,
nonce: u64,
message_id: [u8; 32],
fee_burned: BalanceOf<T>,
},
OperatingModeChanged { mode: BasicOperatingMode },
}
#[pallet::error]
pub enum Error<T> {
InvalidGateway,
InvalidEnvelope,
InvalidNonce,
InvalidPayload,
InvalidChannel,
MaxNonceReached,
InvalidAccountConversion,
Halted,
Verification(VerificationError),
Send(SendError),
ConvertMessage(ConvertMessageError),
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo, PalletError)]
pub enum SendError {
NotApplicable,
NotRoutable,
Transport,
DestinationUnsupported,
ExceedsMaxMessageSize,
MissingArgument,
Fees,
}
impl<T: Config> From<XcmpSendError> for Error<T> {
fn from(e: XcmpSendError) -> Self {
match e {
XcmpSendError::NotApplicable => Error::<T>::Send(SendError::NotApplicable),
XcmpSendError::Unroutable => Error::<T>::Send(SendError::NotRoutable),
XcmpSendError::Transport(_) => Error::<T>::Send(SendError::Transport),
XcmpSendError::DestinationUnsupported =>
Error::<T>::Send(SendError::DestinationUnsupported),
XcmpSendError::ExceedsMaxMessageSize =>
Error::<T>::Send(SendError::ExceedsMaxMessageSize),
XcmpSendError::MissingArgument => Error::<T>::Send(SendError::MissingArgument),
XcmpSendError::Fees => Error::<T>::Send(SendError::Fees),
}
}
}
#[pallet::storage]
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn operating_mode)]
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::submit())]
pub fn submit(origin: OriginFor<T>, message: Message) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
T::Verifier::verify(&message.event_log, &message.proof)
.map_err(|e| Error::<T>::Verification(e))?;
let envelope =
Envelope::try_from(&message.event_log).map_err(|_| Error::<T>::InvalidEnvelope)?;
ensure!(T::GatewayAddress::get() == envelope.gateway, Error::<T>::InvalidGateway);
let channel =
T::ChannelLookup::lookup(envelope.channel_id).ok_or(Error::<T>::InvalidChannel)?;
<Nonce<T>>::try_mutate(envelope.channel_id, |nonce| -> DispatchResult {
if *nonce == u64::MAX {
return Err(Error::<T>::MaxNonceReached.into())
}
if envelope.nonce != nonce.saturating_add(1) {
Err(Error::<T>::InvalidNonce.into())
} else {
*nonce = nonce.saturating_add(1);
Ok(())
}
})?;
let sovereign_account = sibling_sovereign_account::<T>(channel.para_id);
let delivery_cost = Self::calculate_delivery_cost(message.encode().len() as u32);
let amount = T::Token::reducible_balance(
&sovereign_account,
Preservation::Preserve,
Fortitude::Polite,
)
.min(delivery_cost);
if !amount.is_zero() {
T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?;
}
let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref())
.map_err(|_| Error::<T>::InvalidPayload)?;
let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?;
log::info!(
target: LOG_TARGET,
"💫 xcm decoded as {:?} with fee {:?}",
xcm,
fee
);
Self::burn_fees(channel.para_id, fee)?;
let message_id = Self::send_xcm(xcm, channel.para_id)?;
Self::deposit_event(Event::MessageReceived {
channel_id: envelope.channel_id,
nonce: envelope.nonce,
message_id,
fee_burned: fee,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_operating_mode(
origin: OriginFor<T>,
mode: BasicOperatingMode,
) -> DispatchResult {
ensure_root(origin)?;
OperatingMode::<T>::set(mode);
Self::deposit_event(Event::OperatingModeChanged { mode });
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn do_convert(
message_id: H256,
message: VersionedMessage,
) -> Result<(Xcm<()>, BalanceOf<T>), Error<T>> {
let (xcm, fee) = T::MessageConverter::convert(message_id, message)
.map_err(|e| Error::<T>::ConvertMessage(e))?;
Ok((xcm, fee))
}
pub fn send_xcm(xcm: Xcm<()>, dest: ParaId) -> Result<XcmHash, Error<T>> {
let dest = Location::new(1, [Parachain(dest.into())]);
let (xcm_hash, _) = send_xcm::<T::XcmSender>(dest, xcm).map_err(Error::<T>::from)?;
Ok(xcm_hash)
}
pub fn calculate_delivery_cost(length: u32) -> BalanceOf<T> {
let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit());
let len_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
weight_fee
.saturating_add(len_fee)
.saturating_add(T::PricingParameters::get().rewards.local)
}
pub fn burn_fees(para_id: ParaId, fee: BalanceOf<T>) -> DispatchResult {
let dummy_context =
XcmContext { origin: None, message_id: Default::default(), topic: None };
let dest = Location::new(1, [Parachain(para_id.into())]);
let fees = (Location::parent(), fee.saturated_into::<u128>()).into();
T::AssetTransactor::can_check_out(&dest, &fees, &dummy_context).map_err(|error| {
log::error!(
target: LOG_TARGET,
"XCM asset check out failed with error {:?}", error
);
TokenError::FundsUnavailable
})?;
T::AssetTransactor::check_out(&dest, &fees, &dummy_context);
T::AssetTransactor::withdraw_asset(&fees, &dest, None).map_err(|error| {
log::error!(
target: LOG_TARGET,
"XCM asset withdraw failed with error {:?}", error
);
TokenError::FundsUnavailable
})?;
Ok(())
}
}
impl<T: Config> Get<BalanceOf<T>> for Pallet<T> {
fn get() -> BalanceOf<T> {
Self::calculate_delivery_cost(T::MaxMessageSize::get())
}
}
}