referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_system_frontend/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//!
4//! System frontend pallet that acts as the user-facing control-plane for Snowbridge.
5//!
6//! Some operations are delegated to a backend pallet installed on a remote parachain.
7//!
8//! # Extrinsics
9//!
10//! * [`Call::register_token`]: Register Polkadot native asset as a wrapped ERC20 token on Ethereum.
11#![cfg_attr(not(feature = "std"), no_std)]
12#[cfg(test)]
13mod mock;
14
15#[cfg(test)]
16mod tests;
17
18#[cfg(feature = "runtime-benchmarks")]
19mod benchmarking;
20
21pub mod weights;
22pub use weights::*;
23
24pub mod backend_weights;
25pub use backend_weights::*;
26
27use frame_support::{pallet_prelude::*, traits::EnsureOriginWithArg};
28use frame_system::pallet_prelude::*;
29use pallet_asset_conversion::Swap;
30use snowbridge_core::{
31	burn_for_teleport, operating_mode::ExportPausedQuery, reward::MessageId, AssetMetadata,
32	BasicOperatingMode as OperatingMode,
33};
34use sp_std::prelude::*;
35use xcm::{
36	latest::{validate_send, XcmHash},
37	prelude::*,
38};
39use xcm_executor::traits::{FeeManager, FeeReason, TransactAsset};
40
41#[cfg(feature = "runtime-benchmarks")]
42use frame_support::traits::OriginTrait;
43
44pub use pallet::*;
45pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
46
47pub const LOG_TARGET: &str = "snowbridge-system-frontend";
48
49/// Call indices within BridgeHub runtime for dispatchables within `snowbridge-pallet-system-v2`
50#[allow(clippy::large_enum_variant)]
51#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)]
52pub enum BridgeHubRuntime<T: frame_system::Config> {
53	#[codec(index = 90)]
54	EthereumSystem(EthereumSystemCall<T>),
55}
56
57/// Call indices for dispatchables within `snowbridge-pallet-system-v2`
58#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)]
59pub enum EthereumSystemCall<T: frame_system::Config> {
60	#[codec(index = 2)]
61	RegisterToken {
62		sender: Box<VersionedLocation>,
63		asset_id: Box<VersionedLocation>,
64		metadata: AssetMetadata,
65		amount: u128,
66	},
67	#[codec(index = 3)]
68	AddTip { sender: AccountIdOf<T>, message_id: MessageId, amount: u128 },
69}
70
71#[cfg(feature = "runtime-benchmarks")]
72pub trait BenchmarkHelper<O, AccountId>
73where
74	O: OriginTrait,
75{
76	fn make_xcm_origin(location: Location) -> O;
77	fn initialize_storage(asset_location: Location, asset_owner: Location);
78	fn setup_pools(caller: AccountId, asset: Location);
79}
80
81#[frame_support::pallet]
82pub mod pallet {
83	use super::*;
84	use xcm_executor::traits::ConvertLocation;
85	#[pallet::pallet]
86	pub struct Pallet<T>(_);
87
88	#[pallet::config]
89	pub trait Config: frame_system::Config {
90		#[allow(deprecated)]
91		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
92
93		/// Origin check for XCM locations that can register token
94		type RegisterTokenOrigin: EnsureOriginWithArg<
95			Self::RuntimeOrigin,
96			Location,
97			Success = Location,
98		>;
99
100		/// XCM message sender
101		type XcmSender: SendXcm;
102
103		/// To withdraw and deposit an asset.
104		type AssetTransactor: TransactAsset;
105
106		/// To charge XCM delivery fees
107		type XcmExecutor: ExecuteXcm<Self::RuntimeCall> + FeeManager;
108
109		/// Fee asset for the execution cost on ethereum
110		type EthereumLocation: Get<Location>;
111		/// To swap the provided tip asset for
112		type Swap: Swap<Self::AccountId, AssetKind = Location, Balance = u128>;
113
114		/// Location of bridge hub
115		type BridgeHubLocation: Get<Location>;
116
117		/// Universal location of this runtime.
118		type UniversalLocation: Get<InteriorLocation>;
119
120		/// InteriorLocation of this pallet.
121		type PalletLocation: Get<InteriorLocation>;
122
123		type AccountIdConverter: ConvertLocation<Self::AccountId>;
124
125		/// Weights for dispatching XCM to backend implementation of `register_token`
126		type BackendWeightInfo: BackendWeightInfo;
127
128		/// Weights for pallet dispatchables
129		type WeightInfo: WeightInfo;
130
131		/// A set of helper functions for benchmarking.
132		#[cfg(feature = "runtime-benchmarks")]
133		type Helper: BenchmarkHelper<Self::RuntimeOrigin, Self::AccountId>;
134	}
135
136	#[pallet::event]
137	#[pallet::generate_deposit(pub(super) fn deposit_event)]
138	pub enum Event<T: Config> {
139		/// An XCM was sent
140		MessageSent {
141			origin: Location,
142			destination: Location,
143			message: Xcm<()>,
144			message_id: XcmHash,
145		},
146		/// Set OperatingMode
147		ExportOperatingModeChanged { mode: OperatingMode },
148	}
149
150	#[pallet::error]
151	pub enum Error<T> {
152		/// Convert versioned location failure
153		UnsupportedLocationVersion,
154		/// Check location failure, should start from the dispatch origin as owner
155		InvalidAssetOwner,
156		/// Send xcm message failure
157		SendFailure,
158		/// Withdraw fee asset failure
159		FeesNotMet,
160		/// Convert to reanchored location failure
161		LocationConversionFailed,
162		/// Message export is halted
163		Halted,
164		/// The desired destination was unreachable, generally because there is a no way of routing
165		/// to it.
166		Unreachable,
167		/// The asset provided for the tip is unsupported.
168		UnsupportedAsset,
169		/// Unable to withdraw asset.
170		WithdrawError,
171		/// Account could not be converted to a location.
172		InvalidAccount,
173		/// Provided tip asset could not be swapped for ether.
174		SwapError,
175		/// Ether could not be burned.
176		BurnError,
177		/// The tip provided is zero.
178		TipAmountZero,
179	}
180
181	impl<T: Config> From<SendError> for Error<T> {
182		fn from(e: SendError) -> Self {
183			match e {
184				SendError::Fees => Error::<T>::FeesNotMet,
185				SendError::NotApplicable => Error::<T>::Unreachable,
186				_ => Error::<T>::SendFailure,
187			}
188		}
189	}
190
191	/// The current operating mode for exporting to Ethereum.
192	#[pallet::storage]
193	#[pallet::getter(fn export_operating_mode)]
194	pub type ExportOperatingMode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
195
196	#[pallet::call]
197	impl<T: Config> Pallet<T>
198	where
199		<T as frame_system::Config>::AccountId: Into<Location>,
200	{
201		/// Set the operating mode for exporting messages to Ethereum.
202		#[pallet::call_index(0)]
203		#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
204		pub fn set_operating_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
205			ensure_root(origin)?;
206			ExportOperatingMode::<T>::put(mode);
207			Self::deposit_event(Event::ExportOperatingModeChanged { mode });
208			Ok(())
209		}
210
211		/// Initiates the registration for a Polkadot-native token as a wrapped ERC20 token on
212		/// Ethereum.
213		/// - `asset_id`: Location of the asset
214		/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
215		///
216		/// All origins are allowed, however `asset_id` must be a location nested within the origin
217		/// consensus system.
218		#[pallet::call_index(1)]
219		#[pallet::weight(
220			T::WeightInfo::register_token()
221				.saturating_add(T::BackendWeightInfo::transact_register_token())
222				.saturating_add(T::BackendWeightInfo::do_process_message())
223				.saturating_add(T::BackendWeightInfo::commit_single())
224				.saturating_add(T::BackendWeightInfo::submit_delivery_receipt())
225		)]
226		pub fn register_token(
227			origin: OriginFor<T>,
228			asset_id: Box<VersionedLocation>,
229			metadata: AssetMetadata,
230			fee_asset: Asset,
231		) -> DispatchResult {
232			ensure!(!Self::export_operating_mode().is_halted(), Error::<T>::Halted);
233
234			let asset_location: Location =
235				(*asset_id).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
236			let origin_location = T::RegisterTokenOrigin::ensure_origin(origin, &asset_location)?;
237
238			let ether_gained = if origin_location.is_here() {
239				// Root origin/location does not pay any fees/tip.
240				0
241			} else {
242				Self::swap_fee_asset_and_burn(origin_location.clone(), fee_asset)?
243			};
244
245			let call = Self::build_register_token_call(
246				origin_location.clone(),
247				asset_location,
248				metadata,
249				ether_gained,
250			)?;
251
252			Self::send_transact_call(origin_location, call)
253		}
254
255		/// Add an additional relayer tip for a committed message identified by `message_id`.
256		/// The tip asset will be swapped for ether.
257		#[pallet::call_index(2)]
258		#[pallet::weight(
259			T::WeightInfo::add_tip()
260				.saturating_add(T::BackendWeightInfo::transact_add_tip())
261		)]
262		pub fn add_tip(origin: OriginFor<T>, message_id: MessageId, asset: Asset) -> DispatchResult
263		where
264			<T as frame_system::Config>::AccountId: Into<Location>,
265		{
266			let who = ensure_signed(origin)?;
267
268			let ether_gained = Self::swap_fee_asset_and_burn(who.clone().into(), asset)?;
269
270			// Send the tip details to BH to be allocated to the reward in the Inbound/Outbound
271			// pallet
272			let call = Self::build_add_tip_call(who.clone(), message_id.clone(), ether_gained);
273			Self::send_transact_call(who.into(), call)
274		}
275	}
276
277	impl<T: Config> Pallet<T> {
278		fn send_xcm(origin: Location, dest: Location, xcm: Xcm<()>) -> Result<XcmHash, SendError> {
279			let is_waived =
280				<T::XcmExecutor as FeeManager>::is_waived(Some(&origin), FeeReason::ChargeFees);
281			let (ticket, price) = validate_send::<T::XcmSender>(dest, xcm.clone())?;
282			if !is_waived {
283				T::XcmExecutor::charge_fees(origin, price).map_err(|_| SendError::Fees)?;
284			}
285			T::XcmSender::deliver(ticket)
286		}
287
288		/// Swaps a specified tip asset to Ether and then burns the resulting ether for
289		/// teleportation. Returns the amount of Ether gained if successful, or a DispatchError if
290		/// any step fails.
291		fn swap_and_burn(
292			origin: Location,
293			tip_asset_location: Location,
294			ether_location: Location,
295			tip_amount: u128,
296		) -> Result<u128, DispatchError> {
297			// Swap tip asset to ether
298			let swap_path = vec![tip_asset_location.clone(), ether_location.clone()];
299			let who = T::AccountIdConverter::convert_location(&origin)
300				.ok_or(Error::<T>::LocationConversionFailed)?;
301
302			let ether_gained = T::Swap::swap_exact_tokens_for_tokens(
303				who.clone(),
304				swap_path,
305				tip_amount,
306				None, // No minimum amount required
307				who,
308				true,
309			)?;
310
311			// Burn the ether
312			let ether_asset = Asset::from((ether_location.clone(), ether_gained));
313
314			burn_for_teleport::<T::AssetTransactor>(&origin, &ether_asset)
315				.map_err(|_| Error::<T>::BurnError)?;
316
317			Ok(ether_gained)
318		}
319
320		// Build the call to dispatch the `EthereumSystem::register_token` extrinsic on BH
321		fn build_register_token_call(
322			sender: Location,
323			asset: Location,
324			metadata: AssetMetadata,
325			amount: u128,
326		) -> Result<BridgeHubRuntime<T>, Error<T>> {
327			// reanchor locations relative to BH
328			let sender = Self::reanchored(sender)?;
329			let asset = Self::reanchored(asset)?;
330
331			let call = BridgeHubRuntime::EthereumSystem(EthereumSystemCall::RegisterToken {
332				sender: Box::new(VersionedLocation::from(sender)),
333				asset_id: Box::new(VersionedLocation::from(asset)),
334				metadata,
335				amount,
336			});
337
338			Ok(call)
339		}
340
341		// Build the call to dispatch the `EthereumSystem::add_tip` extrinsic on BH
342		fn build_add_tip_call(
343			sender: AccountIdOf<T>,
344			message_id: MessageId,
345			amount: u128,
346		) -> BridgeHubRuntime<T> {
347			BridgeHubRuntime::EthereumSystem(EthereumSystemCall::AddTip {
348				sender,
349				message_id,
350				amount,
351			})
352		}
353
354		fn build_remote_xcm(call: &impl Encode) -> Xcm<()> {
355			Xcm(vec![
356				DescendOrigin(T::PalletLocation::get()),
357				UnpaidExecution { weight_limit: Unlimited, check_origin: None },
358				Transact {
359					origin_kind: OriginKind::Xcm,
360					call: call.encode().into(),
361					fallback_max_weight: None,
362				},
363			])
364		}
365
366		/// Reanchors `location` relative to BridgeHub.
367		fn reanchored(location: Location) -> Result<Location, Error<T>> {
368			location
369				.reanchored(&T::BridgeHubLocation::get(), &T::UniversalLocation::get())
370				.map_err(|_| Error::<T>::LocationConversionFailed)
371		}
372
373		fn swap_fee_asset_and_burn(
374			origin: Location,
375			fee_asset: Asset,
376		) -> Result<u128, DispatchError> {
377			let ether_location = T::EthereumLocation::get();
378			let (fee_asset_location, fee_amount) = match fee_asset {
379				Asset { id: AssetId(ref loc), fun: Fungible(amount) } => (loc, amount),
380				_ => {
381					tracing::debug!(target: LOG_TARGET, ?fee_asset, "error matching fee asset");
382					return Err(Error::<T>::UnsupportedAsset.into())
383				},
384			};
385			if fee_amount == 0 {
386				return Ok(0)
387			}
388
389			let ether_gained = if *fee_asset_location != ether_location {
390				Self::swap_and_burn(
391					origin.clone(),
392					fee_asset_location.clone(),
393					ether_location,
394					fee_amount,
395				)
396				.inspect_err(|&e| {
397					tracing::debug!(target: LOG_TARGET, ?e, "error swapping asset");
398				})?
399			} else {
400				burn_for_teleport::<T::AssetTransactor>(&origin, &fee_asset)
401					.map_err(|_| Error::<T>::BurnError)?;
402				fee_amount
403			};
404			Ok(ether_gained)
405		}
406
407		fn send_transact_call(
408			origin_location: Location,
409			call: BridgeHubRuntime<T>,
410		) -> DispatchResult {
411			let dest = T::BridgeHubLocation::get();
412			let remote_xcm = Self::build_remote_xcm(&call);
413			let message_id = Self::send_xcm(origin_location, dest.clone(), remote_xcm.clone())
414				.map_err(|error| Error::<T>::from(error))?;
415
416			Self::deposit_event(Event::<T>::MessageSent {
417				origin: T::PalletLocation::get().into(),
418				destination: dest,
419				message: remote_xcm,
420				message_id,
421			});
422
423			Ok(())
424		}
425	}
426
427	impl<T: Config> ExportPausedQuery for Pallet<T> {
428		fn is_paused() -> bool {
429			Self::export_operating_mode().is_halted()
430		}
431	}
432}