referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_inbound_queue_v2/
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 AssetHub for further processing.
9//!
10//! Message relayers are rewarded in wrapped Ether that is included within the message. This
11//! wrapped Ether is derived from Ether that the message origin has locked up on Ethereum.
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 to the final destination
23//!   parachain.
24#![cfg_attr(not(feature = "std"), no_std)]
25
26extern crate alloc;
27
28#[cfg(feature = "runtime-benchmarks")]
29mod benchmarking;
30pub mod weights;
31
32#[cfg(test)]
33mod mock;
34
35#[cfg(test)]
36mod test;
37
38pub use crate::weights::WeightInfo;
39use bp_relayers::RewardLedger;
40use frame_system::ensure_signed;
41use snowbridge_core::{
42	reward::{AddTip, AddTipError},
43	sparse_bitmap::{SparseBitmap, SparseBitmapImpl},
44	BasicOperatingMode,
45};
46use snowbridge_inbound_queue_primitives::{
47	v2::{ConvertMessage, ConvertMessageError, Message},
48	EventProof, VerificationError, Verifier,
49};
50use sp_core::H160;
51use sp_runtime::traits::TryConvert;
52use sp_std::prelude::*;
53use xcm::prelude::{ExecuteXcm, Junction::*, Location, SendXcm, *};
54
55pub use pallet::*;
56
57pub const LOG_TARGET: &str = "snowbridge-pallet-inbound-queue-v2";
58
59pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
60
61pub type Nonce<T> = SparseBitmapImpl<crate::NonceBitmap<T>>;
62
63#[frame_support::pallet]
64pub mod pallet {
65	use super::*;
66
67	use frame_support::pallet_prelude::*;
68	use frame_system::pallet_prelude::*;
69
70	#[cfg(feature = "runtime-benchmarks")]
71	use snowbridge_inbound_queue_primitives::EventFixture;
72
73	#[pallet::pallet]
74	pub struct Pallet<T>(_);
75
76	#[cfg(feature = "runtime-benchmarks")]
77	pub trait BenchmarkHelper<T> {
78		fn initialize_storage() -> EventFixture;
79	}
80
81	#[pallet::config]
82	pub trait Config: frame_system::Config {
83		#[allow(deprecated)]
84		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
85		/// The verifier for inbound messages from Ethereum.
86		type Verifier: Verifier;
87		/// XCM message sender.
88		type XcmSender: SendXcm;
89		/// Handler for XCM fees.
90		type XcmExecutor: ExecuteXcm<Self::RuntimeCall>;
91		/// Address of the Gateway contract.
92		#[pallet::constant]
93		type GatewayAddress: Get<H160>;
94		/// AssetHub parachain ID.
95		type AssetHubParaId: Get<u32>;
96		/// Convert a command from Ethereum to an XCM message.
97		type MessageConverter: ConvertMessage;
98		#[cfg(feature = "runtime-benchmarks")]
99		type Helper: BenchmarkHelper<Self>;
100		/// Reward discriminator type.
101		type RewardKind: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone;
102		/// The default RewardKind discriminator for rewards allocated to relayers from this pallet.
103		#[pallet::constant]
104		type DefaultRewardKind: Get<Self::RewardKind>;
105		/// Relayer reward payment.
106		type RewardPayment: RewardLedger<Self::AccountId, Self::RewardKind, u128>;
107		/// AccountId to Location converter
108		type AccountToLocation: for<'a> TryConvert<&'a Self::AccountId, Location>;
109		type WeightInfo: WeightInfo;
110	}
111
112	#[pallet::event]
113	#[pallet::generate_deposit(pub(super) fn deposit_event)]
114	pub enum Event<T: Config> {
115		/// A message was received from Ethereum
116		MessageReceived {
117			/// The message nonce
118			nonce: u64,
119			/// ID of the XCM message which was forwarded to the final destination parachain
120			message_id: [u8; 32],
121		},
122		/// Set OperatingMode
123		OperatingModeChanged { mode: BasicOperatingMode },
124	}
125
126	#[pallet::error]
127	pub enum Error<T> {
128		/// Message came from an invalid outbound channel on the Ethereum side.
129		InvalidGateway,
130		/// Account could not be converted to bytes
131		InvalidAccount,
132		/// Message has an invalid envelope.
133		InvalidMessage,
134		/// Message has an unexpected nonce.
135		InvalidNonce,
136		/// Fee provided is invalid.
137		InvalidFee,
138		/// Message has an invalid payload.
139		InvalidPayload,
140		/// Message channel is invalid
141		InvalidChannel,
142		/// The max nonce for the type has been reached
143		MaxNonceReached,
144		/// Cannot convert location
145		InvalidAccountConversion,
146		/// Invalid network specified
147		InvalidNetwork,
148		/// Pallet is halted
149		Halted,
150		/// The operation required fees to be paid which the initiator could not meet.
151		FeesNotMet,
152		/// The desired destination was unreachable, generally because there is a no way of routing
153		/// to it.
154		Unreachable,
155		/// There was some other issue (i.e. not to do with routing) in sending the message.
156		/// Perhaps a lack of space for buffering the message.
157		SendFailure,
158		/// Invalid foreign ERC-20 token ID
159		InvalidAsset,
160		/// Cannot reachor a foreign ERC-20 asset location.
161		CannotReanchor,
162		/// Message verification error
163		Verification(VerificationError),
164	}
165
166	impl<T: Config> From<SendError> for Error<T> {
167		fn from(e: SendError) -> Self {
168			match e {
169				SendError::Fees => Error::<T>::FeesNotMet,
170				SendError::NotApplicable => Error::<T>::Unreachable,
171				_ => Error::<T>::SendFailure,
172			}
173		}
174	}
175
176	impl<T: Config> From<ConvertMessageError> for Error<T> {
177		fn from(e: ConvertMessageError) -> Self {
178			match e {
179				ConvertMessageError::InvalidAsset => Error::<T>::InvalidAsset,
180				ConvertMessageError::CannotReanchor => Error::<T>::CannotReanchor,
181				ConvertMessageError::InvalidNetwork => Error::<T>::InvalidNetwork,
182			}
183		}
184	}
185
186	/// StorageMap used for encoding a SparseBitmapImpl that tracks whether a specific nonce has
187	/// been processed or not. Message nonces are unique and never repeated.
188	#[pallet::storage]
189	pub type NonceBitmap<T: Config> = StorageMap<_, Twox64Concat, u64, u128, ValueQuery>;
190
191	/// The current operating mode of the pallet.
192	#[pallet::storage]
193	pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
194
195	/// Keep track of tips added for a message as an additional relayer incentivization. The
196	/// key for the storage map is the nonce of the message to which the tip should be added.
197	/// The value is the tip amount, in Ether.
198	#[pallet::storage]
199	pub type Tips<T: Config> = StorageMap<_, Blake2_128Concat, u64, u128, OptionQuery>;
200
201	#[pallet::call]
202	impl<T: Config> Pallet<T> {
203		/// Submit an inbound message originating from the Gateway contract on Ethereum
204		#[pallet::call_index(0)]
205		#[pallet::weight(T::WeightInfo::submit())]
206		pub fn submit(origin: OriginFor<T>, event: Box<EventProof>) -> DispatchResult {
207			let who = ensure_signed(origin)?;
208			ensure!(!OperatingMode::<T>::get().is_halted(), Error::<T>::Halted);
209
210			// submit message for verification
211			T::Verifier::verify(&event.event_log, &event.proof)
212				.map_err(|e| Error::<T>::Verification(e))?;
213
214			// Decode event log into a bridge message
215			let message =
216				Message::try_from(&event.event_log).map_err(|_| Error::<T>::InvalidMessage)?;
217
218			Self::process_message(who, message)
219		}
220
221		/// Halt or resume all pallet operations. May only be called by root.
222		#[pallet::call_index(1)]
223		#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
224		pub fn set_operating_mode(
225			origin: OriginFor<T>,
226			mode: BasicOperatingMode,
227		) -> DispatchResult {
228			ensure_root(origin)?;
229			OperatingMode::<T>::set(mode);
230			Self::deposit_event(Event::OperatingModeChanged { mode });
231			Ok(())
232		}
233	}
234
235	impl<T: Config> Pallet<T> {
236		pub fn process_message(relayer: T::AccountId, message: Message) -> DispatchResult {
237			// Verify that the message was submitted from the known Gateway contract
238			ensure!(T::GatewayAddress::get() == message.gateway, Error::<T>::InvalidGateway);
239
240			let (nonce, relayer_fee) = (message.nonce, message.relayer_fee);
241
242			// Verify the message has not been processed
243			ensure!(!Nonce::<T>::get(nonce), Error::<T>::InvalidNonce);
244
245			let xcm =
246				T::MessageConverter::convert(message).map_err(|error| Error::<T>::from(error))?;
247
248			// Forward XCM to AH
249			let dest = Location::new(1, [Parachain(T::AssetHubParaId::get())]);
250
251			// Mark message as received
252			Nonce::<T>::set(nonce);
253
254			let message_id =
255				Self::send_xcm(dest.clone(), &relayer, xcm.clone()).map_err(|error| {
256					tracing::error!(target: LOG_TARGET, ?error, ?dest, ?xcm, "XCM send failed with error");
257					Error::<T>::from(error)
258				})?;
259
260			// Pay relayer reward
261			if !relayer_fee.is_zero() {
262				T::RewardPayment::register_reward(
263					&relayer,
264					T::DefaultRewardKind::get(),
265					relayer_fee,
266				);
267			}
268
269			Self::deposit_event(Event::MessageReceived { nonce, message_id });
270
271			Ok(())
272		}
273
274		fn send_xcm(
275			dest: Location,
276			fee_payer: &T::AccountId,
277			xcm: Xcm<()>,
278		) -> Result<XcmHash, SendError> {
279			let (ticket, fee) = validate_send::<T::XcmSender>(dest, xcm)?;
280			let fee_payer = T::AccountToLocation::try_convert(fee_payer).map_err(|err| {
281				tracing::error!(
282					target: LOG_TARGET,
283					?err,
284					"Failed to convert account to XCM location",
285				);
286				SendError::NotApplicable
287			})?;
288			T::XcmExecutor::charge_fees(fee_payer.clone(), fee.clone()).map_err(|error| {
289				tracing::error!(
290					target: LOG_TARGET,
291					?error,
292					"Charging fees failed with error",
293				);
294				SendError::Fees
295			})?;
296			T::XcmSender::deliver(ticket)
297		}
298	}
299
300	impl<T: Config> AddTip for Pallet<T> {
301		fn add_tip(nonce: u64, amount: u128) -> Result<(), AddTipError> {
302			ensure!(amount > 0, AddTipError::AmountZero);
303			// If the nonce is already processed, return an error
304			ensure!(!Nonce::<T>::get(nonce.into()), AddTipError::NonceConsumed);
305			// Otherwise add the tip.
306			Tips::<T>::mutate(nonce, |tip| {
307				*tip = Some(tip.unwrap_or_default().saturating_add(amount));
308			});
309			return Ok(())
310		}
311	}
312}