Skip to main content

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