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