Skip to main content

snowbridge_pallet_system_frontend/
lib.rs

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