snowbridge_pallet_inbound_queue/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
25
26mod envelope;
27
28#[cfg(feature = "runtime-benchmarks")]
29mod benchmarking;
30
31pub mod weights;
32
33#[cfg(test)]
34mod mock;
35
36#[cfg(test)]
37mod test;
38
39use codec::{Decode, DecodeAll, Encode};
40use envelope::Envelope;
41use frame_support::{
42 traits::{
43 fungible::{Inspect, Mutate},
44 tokens::{Fortitude, Preservation},
45 },
46 weights::WeightToFee,
47 PalletError,
48};
49use frame_system::ensure_signed;
50use scale_info::TypeInfo;
51use sp_core::H160;
52use sp_runtime::traits::Zero;
53use sp_std::vec;
54use xcm::prelude::{
55 send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm, XcmContext, XcmHash,
56};
57use xcm_executor::traits::TransactAsset;
58
59use snowbridge_core::{
60 sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters,
61 StaticLookup,
62};
63use snowbridge_inbound_queue_primitives::{
64 v1::{ConvertMessage, ConvertMessageError, VersionedMessage},
65 EventProof, VerificationError, Verifier,
66};
67
68use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError};
69
70pub use weights::WeightInfo;
71
72#[cfg(feature = "runtime-benchmarks")]
73use snowbridge_beacon_primitives::BeaconHeader;
74
75type BalanceOf<T> =
76 <<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
77
78pub use pallet::*;
79
80pub const LOG_TARGET: &str = "snowbridge-inbound-queue";
81
82#[frame_support::pallet]
83pub mod pallet {
84 use super::*;
85
86 use frame_support::pallet_prelude::*;
87 use frame_system::pallet_prelude::*;
88 use sp_core::H256;
89
90 #[pallet::pallet]
91 pub struct Pallet<T>(_);
92
93 #[cfg(feature = "runtime-benchmarks")]
94 pub trait BenchmarkHelper<T> {
95 fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256);
96 }
97
98 #[pallet::config]
99 pub trait Config: frame_system::Config {
100 #[allow(deprecated)]
101 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
102
103 type Verifier: Verifier;
105
106 type Token: Mutate<Self::AccountId> + Inspect<Self::AccountId>;
108
109 type XcmSender: SendXcm;
111
112 #[pallet::constant]
114 type GatewayAddress: Get<H160>;
115
116 type MessageConverter: ConvertMessage<
118 AccountId = Self::AccountId,
119 Balance = BalanceOf<Self>,
120 >;
121
122 type ChannelLookup: StaticLookup<Source = ChannelId, Target = Channel>;
124
125 type PricingParameters: Get<PricingParameters<BalanceOf<Self>>>;
127
128 type WeightInfo: WeightInfo;
129
130 #[cfg(feature = "runtime-benchmarks")]
131 type Helper: BenchmarkHelper<Self>;
132
133 type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
135
136 type LengthToFee: WeightToFee<Balance = BalanceOf<Self>>;
138
139 type MaxMessageSize: Get<u32>;
141
142 type AssetTransactor: TransactAsset;
144 }
145
146 #[pallet::hooks]
147 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
148
149 #[pallet::event]
150 #[pallet::generate_deposit(pub(super) fn deposit_event)]
151 pub enum Event<T: Config> {
152 MessageReceived {
154 channel_id: ChannelId,
156 nonce: u64,
158 message_id: [u8; 32],
160 fee_burned: BalanceOf<T>,
162 },
163 OperatingModeChanged { mode: BasicOperatingMode },
165 }
166
167 #[pallet::error]
168 pub enum Error<T> {
169 InvalidGateway,
171 InvalidEnvelope,
173 InvalidNonce,
175 InvalidPayload,
177 InvalidChannel,
179 MaxNonceReached,
181 InvalidAccountConversion,
183 Halted,
185 Verification(VerificationError),
187 Send(SendError),
189 ConvertMessage(ConvertMessageError),
191 }
192
193 #[derive(
194 Clone, Encode, Decode, DecodeWithMemTracking, Eq, PartialEq, Debug, TypeInfo, PalletError,
195 )]
196 pub enum SendError {
197 NotApplicable,
198 NotRoutable,
199 Transport,
200 DestinationUnsupported,
201 ExceedsMaxMessageSize,
202 MissingArgument,
203 Fees,
204 }
205
206 impl<T: Config> From<XcmpSendError> for Error<T> {
207 fn from(e: XcmpSendError) -> Self {
208 match e {
209 XcmpSendError::NotApplicable => Error::<T>::Send(SendError::NotApplicable),
210 XcmpSendError::Unroutable => Error::<T>::Send(SendError::NotRoutable),
211 XcmpSendError::Transport(_) => Error::<T>::Send(SendError::Transport),
212 XcmpSendError::DestinationUnsupported =>
213 Error::<T>::Send(SendError::DestinationUnsupported),
214 XcmpSendError::ExceedsMaxMessageSize =>
215 Error::<T>::Send(SendError::ExceedsMaxMessageSize),
216 XcmpSendError::MissingArgument => Error::<T>::Send(SendError::MissingArgument),
217 XcmpSendError::Fees => Error::<T>::Send(SendError::Fees),
218 }
219 }
220 }
221
222 #[pallet::storage]
224 pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
225
226 #[pallet::storage]
228 #[pallet::getter(fn operating_mode)]
229 pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
230
231 #[pallet::call]
232 impl<T: Config> Pallet<T> {
233 #[pallet::call_index(0)]
235 #[pallet::weight(T::WeightInfo::submit())]
236 pub fn submit(origin: OriginFor<T>, event: EventProof) -> DispatchResult {
237 let who = ensure_signed(origin)?;
238 ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
239
240 T::Verifier::verify(&event.event_log, &event.proof)
242 .map_err(|e| Error::<T>::Verification(e))?;
243
244 let envelope =
246 Envelope::try_from(&event.event_log).map_err(|_| Error::<T>::InvalidEnvelope)?;
247
248 ensure!(T::GatewayAddress::get() == envelope.gateway, Error::<T>::InvalidGateway);
250
251 let channel =
253 T::ChannelLookup::lookup(envelope.channel_id).ok_or(Error::<T>::InvalidChannel)?;
254
255 <Nonce<T>>::try_mutate(envelope.channel_id, |nonce| -> DispatchResult {
257 if *nonce == u64::MAX {
258 return Err(Error::<T>::MaxNonceReached.into())
259 }
260 if envelope.nonce != nonce.saturating_add(1) {
261 Err(Error::<T>::InvalidNonce.into())
262 } else {
263 *nonce = nonce.saturating_add(1);
264 Ok(())
265 }
266 })?;
267
268 let sovereign_account = sibling_sovereign_account::<T>(channel.para_id);
271 let delivery_cost = Self::calculate_delivery_cost(event.encode().len() as u32);
272 let amount = T::Token::reducible_balance(
273 &sovereign_account,
274 Preservation::Preserve,
275 Fortitude::Polite,
276 )
277 .min(delivery_cost);
278 if !amount.is_zero() {
279 T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?;
280 }
281
282 let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref())
284 .map_err(|_| Error::<T>::InvalidPayload)?;
285
286 let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?;
288
289 log::info!(
290 target: LOG_TARGET,
291 "💫 xcm decoded as {:?} with fee {:?}",
292 xcm,
293 fee
294 );
295
296 Self::burn_fees(channel.para_id, fee)?;
298
299 let message_id = Self::send_xcm(xcm, channel.para_id)?;
301
302 Self::deposit_event(Event::MessageReceived {
303 channel_id: envelope.channel_id,
304 nonce: envelope.nonce,
305 message_id,
306 fee_burned: fee,
307 });
308
309 Ok(())
310 }
311
312 #[pallet::call_index(1)]
314 #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
315 pub fn set_operating_mode(
316 origin: OriginFor<T>,
317 mode: BasicOperatingMode,
318 ) -> DispatchResult {
319 ensure_root(origin)?;
320 OperatingMode::<T>::set(mode);
321 Self::deposit_event(Event::OperatingModeChanged { mode });
322 Ok(())
323 }
324 }
325
326 impl<T: Config> Pallet<T> {
327 pub fn do_convert(
328 message_id: H256,
329 message: VersionedMessage,
330 ) -> Result<(Xcm<()>, BalanceOf<T>), Error<T>> {
331 let (xcm, fee) = T::MessageConverter::convert(message_id, message)
332 .map_err(|e| Error::<T>::ConvertMessage(e))?;
333 Ok((xcm, fee))
334 }
335
336 pub fn send_xcm(xcm: Xcm<()>, dest: ParaId) -> Result<XcmHash, Error<T>> {
337 let dest = Location::new(1, [Parachain(dest.into())]);
338 let (xcm_hash, _) = send_xcm::<T::XcmSender>(dest, xcm).map_err(Error::<T>::from)?;
339 Ok(xcm_hash)
340 }
341
342 pub fn calculate_delivery_cost(length: u32) -> BalanceOf<T> {
343 let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit());
344 let len_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
345 weight_fee
346 .saturating_add(len_fee)
347 .saturating_add(T::PricingParameters::get().rewards.local)
348 }
349
350 pub fn burn_fees(para_id: ParaId, fee: BalanceOf<T>) -> DispatchResult {
352 let dummy_context =
353 XcmContext { origin: None, message_id: Default::default(), topic: None };
354 let dest = Location::new(1, [Parachain(para_id.into())]);
355 let fees = (Location::parent(), fee.saturated_into::<u128>()).into();
356 T::AssetTransactor::can_check_out(&dest, &fees, &dummy_context).map_err(|error| {
357 log::error!(
358 target: LOG_TARGET,
359 "XCM asset check out failed with error {:?}", error
360 );
361 TokenError::FundsUnavailable
362 })?;
363 T::AssetTransactor::check_out(&dest, &fees, &dummy_context);
364 T::AssetTransactor::withdraw_asset(&fees, &dest, None).map_err(|error| {
365 log::error!(
366 target: LOG_TARGET,
367 "XCM asset withdraw failed with error {:?}", error
368 );
369 TokenError::FundsUnavailable
370 })?;
371 Ok(())
372 }
373 }
374
375 impl<T: Config> Get<BalanceOf<T>> for Pallet<T> {
377 fn get() -> BalanceOf<T> {
378 Self::calculate_delivery_cost(T::MaxMessageSize::get())
380 }
381 }
382}