snowbridge_pallet_system_frontend/
lib.rs1#![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#[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#[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 type RegisterTokenOrigin: EnsureOriginWithArg<
94 Self::RuntimeOrigin,
95 Location,
96 Success = Location,
97 >;
98
99 type XcmSender: SendXcm;
101
102 type AssetTransactor: TransactAsset;
104
105 type XcmExecutor: ExecuteXcm<Self::RuntimeCall> + FeeManager;
107
108 type EthereumLocation: Get<Location>;
110 type Swap: Swap<Self::AccountId, AssetKind = Location, Balance = u128>;
112
113 type BridgeHubLocation: Get<Location>;
115
116 type UniversalLocation: Get<InteriorLocation>;
118
119 type PalletLocation: Get<InteriorLocation>;
121
122 type AccountIdConverter: ConvertLocation<Self::AccountId>;
123
124 type BackendWeightInfo: BackendWeightInfo;
126
127 type WeightInfo: WeightInfo;
129
130 #[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 MessageSent {
140 origin: Location,
141 destination: Location,
142 message: Xcm<()>,
143 message_id: XcmHash,
144 },
145 ExportOperatingModeChanged { mode: OperatingMode },
147 }
148
149 #[pallet::error]
150 pub enum Error<T> {
151 UnsupportedLocationVersion,
153 InvalidAssetOwner,
155 SendFailure,
157 FeesNotMet,
159 LocationConversionFailed,
161 Halted,
163 Unreachable,
166 UnsupportedAsset,
168 WithdrawError,
170 InvalidAccount,
172 SwapError,
174 BurnError,
176 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 #[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 #[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 #[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 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 #[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 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 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 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, who,
307 true,
308 )?;
309
310 let ether_asset = Asset::from((ether_location.clone(), ether_gained));
312
313 burn_for_teleport::<T::AssetTransactor>(&origin, ðer_asset)
314 .map_err(|_| Error::<T>::BurnError)?;
315
316 Ok(ether_gained)
317 }
318
319 fn build_register_token_call(
321 sender: Location,
322 asset: Location,
323 metadata: AssetMetadata,
324 amount: u128,
325 ) -> Result<BridgeHubRuntime<T>, Error<T>> {
326 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 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 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}