referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_inbound_queue/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Inbound Queue
4//!
5//! # Overview
6//!
7//! Receives messages emitted by the Gateway contract on Ethereum, whereupon they are verified,
8//! translated to XCM, and finally sent to their final destination parachain.
9//!
10//! The message relayers are rewarded using native currency from the sovereign account of the
11//! destination parachain.
12//!
13//! # Extrinsics
14//!
15//! ## Governance
16//!
17//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
18//!   processing of inbound messages.
19//!
20//! ## Message Submission
21//!
22//! * [`Call::submit`]: Submit a message for verification and dispatch the final destination
23//!   parachain.
24#![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		/// The verifier for inbound messages from Ethereum
104		type Verifier: Verifier;
105
106		/// Message relayers are rewarded with this asset
107		type Token: Mutate<Self::AccountId> + Inspect<Self::AccountId>;
108
109		/// XCM message sender
110		type XcmSender: SendXcm;
111
112		// Address of the Gateway contract
113		#[pallet::constant]
114		type GatewayAddress: Get<H160>;
115
116		/// Convert inbound message to XCM
117		type MessageConverter: ConvertMessage<
118			AccountId = Self::AccountId,
119			Balance = BalanceOf<Self>,
120		>;
121
122		/// Lookup a channel descriptor
123		type ChannelLookup: StaticLookup<Source = ChannelId, Target = Channel>;
124
125		/// Lookup pricing parameters
126		type PricingParameters: Get<PricingParameters<BalanceOf<Self>>>;
127
128		type WeightInfo: WeightInfo;
129
130		#[cfg(feature = "runtime-benchmarks")]
131		type Helper: BenchmarkHelper<Self>;
132
133		/// Convert a weight value into deductible balance type.
134		type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
135
136		/// Convert a length value into deductible balance type
137		type LengthToFee: WeightToFee<Balance = BalanceOf<Self>>;
138
139		/// The upper limit here only used to estimate delivery cost
140		type MaxMessageSize: Get<u32>;
141
142		/// To withdraw and deposit an asset.
143		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		/// A message was received from Ethereum
153		MessageReceived {
154			/// The message channel
155			channel_id: ChannelId,
156			/// The message nonce
157			nonce: u64,
158			/// ID of the XCM message which was forwarded to the final destination parachain
159			message_id: [u8; 32],
160			/// Fee burned for the teleport
161			fee_burned: BalanceOf<T>,
162		},
163		/// Set OperatingMode
164		OperatingModeChanged { mode: BasicOperatingMode },
165	}
166
167	#[pallet::error]
168	pub enum Error<T> {
169		/// Message came from an invalid outbound channel on the Ethereum side.
170		InvalidGateway,
171		/// Message has an invalid envelope.
172		InvalidEnvelope,
173		/// Message has an unexpected nonce.
174		InvalidNonce,
175		/// Message has an invalid payload.
176		InvalidPayload,
177		/// Message channel is invalid
178		InvalidChannel,
179		/// The max nonce for the type has been reached
180		MaxNonceReached,
181		/// Cannot convert location
182		InvalidAccountConversion,
183		/// Pallet is halted
184		Halted,
185		/// Message verification error,
186		Verification(VerificationError),
187		/// XCMP send failure
188		Send(SendError),
189		/// Message conversion error
190		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	/// The current nonce for each channel
223	#[pallet::storage]
224	pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
225
226	/// The current operating mode of the pallet.
227	#[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		/// Submit an inbound message originating from the Gateway contract on Ethereum
234		#[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			// submit message to verifier for verification
241			T::Verifier::verify(&event.event_log, &event.proof)
242				.map_err(|e| Error::<T>::Verification(e))?;
243
244			// Decode event log into an Envelope
245			let envelope =
246				Envelope::try_from(&event.event_log).map_err(|_| Error::<T>::InvalidEnvelope)?;
247
248			// Verify that the message was submitted from the known Gateway contract
249			ensure!(T::GatewayAddress::get() == envelope.gateway, Error::<T>::InvalidGateway);
250
251			// Retrieve the registered channel for this message
252			let channel =
253				T::ChannelLookup::lookup(envelope.channel_id).ok_or(Error::<T>::InvalidChannel)?;
254
255			// Verify message nonce
256			<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			// Reward relayer from the sovereign account of the destination parachain, only if funds
269			// are available
270			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			// Decode payload into `VersionedMessage`
283			let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref())
284				.map_err(|_| Error::<T>::InvalidPayload)?;
285
286			// Decode message into XCM
287			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			// Burning fees for teleport
297			Self::burn_fees(channel.para_id, fee)?;
298
299			// Attempt to send XCM to a dest parachain
300			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		/// Halt or resume all pallet operations. May only be called by root.
313		#[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		/// Burn the amount of the fee embedded into the XCM for teleports
351		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	/// API for accessing the delivery cost of a message
376	impl<T: Config> Get<BalanceOf<T>> for Pallet<T> {
377		fn get() -> BalanceOf<T> {
378			// Cost here based on MaxMessagePayloadSize(the worst case)
379			Self::calculate_delivery_cost(T::MaxMessageSize::get())
380		}
381	}
382}