1#![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#[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#[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 type RegisterTokenOrigin: EnsureOriginWithArg<
95 Self::RuntimeOrigin,
96 Location,
97 Success = Location,
98 >;
99
100 type XcmSender: SendXcm;
102
103 type AssetTransactor: TransactAsset;
105
106 type XcmExecutor: ExecuteXcm<Self::RuntimeCall> + FeeManager;
108
109 type EthereumLocation: Get<Location>;
111 type Swap: Swap<Self::AccountId, AssetKind = Location, Balance = u128>;
113
114 type BridgeHubLocation: Get<Location>;
116
117 type UniversalLocation: Get<InteriorLocation>;
119
120 type PalletLocation: Get<InteriorLocation>;
122
123 type AccountIdConverter: ConvertLocation<Self::AccountId>;
124
125 type BackendWeightInfo: BackendWeightInfo;
127
128 type WeightInfo: WeightInfo;
130
131 #[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 MessageSent {
141 origin: Location,
142 destination: Location,
143 message: Xcm<()>,
144 message_id: XcmHash,
145 },
146 ExportOperatingModeChanged { mode: OperatingMode },
148 }
149
150 #[pallet::error]
151 pub enum Error<T> {
152 UnsupportedLocationVersion,
154 InvalidAssetOwner,
156 SendFailure,
158 FeesNotMet,
160 LocationConversionFailed,
162 Halted,
164 Unreachable,
167 UnsupportedAsset,
169 WithdrawError,
171 InvalidAccount,
173 SwapError,
175 BurnError,
177 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 #[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 #[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 #[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 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 #[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 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 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 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, who,
308 true,
309 )?;
310
311 let ether_asset = Asset::from((ether_location.clone(), ether_gained));
313
314 burn_for_teleport::<T::AssetTransactor>(&origin, ðer_asset)
315 .map_err(|_| Error::<T>::BurnError)?;
316
317 Ok(ether_gained)
318 }
319
320 fn build_register_token_call(
322 sender: Location,
323 asset: Location,
324 metadata: AssetMetadata,
325 amount: u128,
326 ) -> Result<BridgeHubRuntime<T>, Error<T>> {
327 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 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 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}