referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_system/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Governance API for controlling the Ethereum side of the bridge
4//!
5//! # Extrinsics
6//!
7//! ## Governance
8//!
9//! Only Polkadot governance itself can call these extrinsics. Delivery fees are waived.
10//!
11//! * [`Call::upgrade`]`: Upgrade the gateway contract
12//! * [`Call::set_operating_mode`]: Update the operating mode of the gateway contract
13//!
14//! ## Polkadot-native tokens on Ethereum
15//!
16//! Tokens deposited on AssetHub pallet can be bridged to Ethereum as wrapped ERC20 tokens. As a
17//! prerequisite, the token should be registered first.
18//!
19//! * [`Call::register_token`]: Register a token location as a wrapped ERC20 contract on Ethereum.
20#![cfg_attr(not(feature = "std"), no_std)]
21#[cfg(test)]
22mod mock;
23
24#[cfg(test)]
25mod tests;
26
27#[cfg(feature = "runtime-benchmarks")]
28mod benchmarking;
29pub mod migration;
30
31pub mod api;
32pub mod weights;
33pub use weights::*;
34
35use frame_support::{
36	pallet_prelude::*,
37	traits::{
38		fungible::{Inspect, Mutate},
39		tokens::Preservation,
40		Contains, EnsureOrigin,
41	},
42};
43use frame_system::pallet_prelude::*;
44use snowbridge_core::{
45	meth, AgentId, AssetMetadata, Channel, ChannelId, ParaId,
46	PricingParameters as PricingParametersRecord, TokenId, TokenIdOf, PRIMARY_GOVERNANCE_CHANNEL,
47	SECONDARY_GOVERNANCE_CHANNEL,
48};
49use snowbridge_outbound_queue_primitives::{
50	v1::{Command, Initializer, Message, SendMessage},
51	OperatingMode, SendError,
52};
53use sp_core::{RuntimeDebug, H160, H256};
54use sp_io::hashing::blake2_256;
55use sp_runtime::{traits::MaybeConvert, DispatchError, SaturatedConversion};
56use sp_std::prelude::*;
57use xcm::prelude::*;
58use xcm_executor::traits::ConvertLocation;
59
60#[cfg(feature = "runtime-benchmarks")]
61use frame_support::traits::OriginTrait;
62
63pub use pallet::*;
64
65pub type BalanceOf<T> =
66	<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
67pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
68pub type PricingParametersOf<T> = PricingParametersRecord<BalanceOf<T>>;
69
70/// Hash the location to produce an agent id
71pub fn agent_id_of<T: Config>(location: &Location) -> Result<H256, DispatchError> {
72	T::AgentIdOf::convert_location(location).ok_or(Error::<T>::LocationConversionFailed.into())
73}
74
75#[cfg(feature = "runtime-benchmarks")]
76pub trait BenchmarkHelper<O>
77where
78	O: OriginTrait,
79{
80	fn make_xcm_origin(location: Location) -> O;
81}
82
83/// Whether a fee should be withdrawn to an account for sending an outbound message
84#[derive(Clone, PartialEq, RuntimeDebug)]
85pub enum PaysFee<T>
86where
87	T: Config,
88{
89	/// Fully charge includes (local + remote fee)
90	Yes(AccountIdOf<T>),
91	/// Partially charge includes local fee only
92	Partial(AccountIdOf<T>),
93	/// No charge
94	No,
95}
96
97#[frame_support::pallet]
98pub mod pallet {
99	use frame_support::dispatch::PostDispatchInfo;
100	use snowbridge_core::StaticLookup;
101	use sp_core::U256;
102
103	use super::*;
104
105	#[pallet::pallet]
106	#[pallet::storage_version(migration::STORAGE_VERSION)]
107	pub struct Pallet<T>(_);
108
109	#[pallet::config]
110	pub trait Config: frame_system::Config {
111		#[allow(deprecated)]
112		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
113
114		/// Send messages to Ethereum
115		type OutboundQueue: SendMessage<Balance = BalanceOf<Self>>;
116
117		/// Origin check for XCM locations that can create agents
118		type SiblingOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Location>;
119
120		/// Converts Location to AgentId
121		type AgentIdOf: ConvertLocation<AgentId>;
122
123		/// Token reserved for control operations
124		type Token: Mutate<Self::AccountId>;
125
126		/// TreasuryAccount to collect fees
127		#[pallet::constant]
128		type TreasuryAccount: Get<Self::AccountId>;
129
130		/// Number of decimal places of local currency
131		type DefaultPricingParameters: Get<PricingParametersOf<Self>>;
132
133		/// Cost of delivering a message from Ethereum
134		#[pallet::constant]
135		type InboundDeliveryCost: Get<BalanceOf<Self>>;
136
137		type WeightInfo: WeightInfo;
138
139		/// This chain's Universal Location.
140		type UniversalLocation: Get<InteriorLocation>;
141
142		// The bridges configured Ethereum location
143		type EthereumLocation: Get<Location>;
144
145		#[cfg(feature = "runtime-benchmarks")]
146		type Helper: BenchmarkHelper<Self::RuntimeOrigin>;
147	}
148
149	#[pallet::event]
150	#[pallet::generate_deposit(pub(super) fn deposit_event)]
151	pub enum Event<T: Config> {
152		/// An Upgrade message was sent to the Gateway
153		Upgrade {
154			impl_address: H160,
155			impl_code_hash: H256,
156			initializer_params_hash: Option<H256>,
157		},
158		/// An CreateAgent message was sent to the Gateway
159		CreateAgent {
160			location: Box<Location>,
161			agent_id: AgentId,
162		},
163		/// An CreateChannel message was sent to the Gateway
164		CreateChannel {
165			channel_id: ChannelId,
166			agent_id: AgentId,
167		},
168		/// An UpdateChannel message was sent to the Gateway
169		UpdateChannel {
170			channel_id: ChannelId,
171			mode: OperatingMode,
172		},
173		/// An SetOperatingMode message was sent to the Gateway
174		SetOperatingMode {
175			mode: OperatingMode,
176		},
177		/// An TransferNativeFromAgent message was sent to the Gateway
178		TransferNativeFromAgent {
179			agent_id: AgentId,
180			recipient: H160,
181			amount: u128,
182		},
183		/// A SetTokenTransferFees message was sent to the Gateway
184		SetTokenTransferFees {
185			create_asset_xcm: u128,
186			transfer_asset_xcm: u128,
187			register_token: U256,
188		},
189		PricingParametersChanged {
190			params: PricingParametersOf<T>,
191		},
192		/// Register Polkadot-native token as a wrapped ERC20 token on Ethereum
193		RegisterToken {
194			/// Location of Polkadot-native token
195			location: VersionedLocation,
196			/// ID of Polkadot-native token on Ethereum
197			foreign_token_id: H256,
198		},
199	}
200
201	#[pallet::error]
202	pub enum Error<T> {
203		LocationConversionFailed,
204		AgentAlreadyCreated,
205		NoAgent,
206		ChannelAlreadyCreated,
207		NoChannel,
208		UnsupportedLocationVersion,
209		InvalidLocation,
210		Send(SendError),
211		InvalidTokenTransferFees,
212		InvalidPricingParameters,
213		InvalidUpgradeParameters,
214	}
215
216	/// The set of registered agents
217	#[pallet::storage]
218	#[pallet::getter(fn agents)]
219	pub type Agents<T: Config> = StorageMap<_, Twox64Concat, AgentId, (), OptionQuery>;
220
221	/// The set of registered channels
222	#[pallet::storage]
223	#[pallet::getter(fn channels)]
224	pub type Channels<T: Config> = StorageMap<_, Twox64Concat, ChannelId, Channel, OptionQuery>;
225
226	#[pallet::storage]
227	#[pallet::getter(fn parameters)]
228	pub type PricingParameters<T: Config> =
229		StorageValue<_, PricingParametersOf<T>, ValueQuery, T::DefaultPricingParameters>;
230
231	/// Lookup table for foreign token ID to native location relative to ethereum
232	#[pallet::storage]
233	pub type ForeignToNativeId<T: Config> =
234		StorageMap<_, Blake2_128Concat, TokenId, Location, OptionQuery>;
235
236	#[pallet::genesis_config]
237	#[derive(frame_support::DefaultNoBound)]
238	pub struct GenesisConfig<T: Config> {
239		// Own parachain id
240		pub para_id: ParaId,
241		// AssetHub's parachain id
242		pub asset_hub_para_id: ParaId,
243		#[serde(skip)]
244		pub _config: PhantomData<T>,
245	}
246
247	#[pallet::genesis_build]
248	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
249		fn build(&self) {
250			Pallet::<T>::initialize(self.para_id, self.asset_hub_para_id).expect("infallible; qed");
251		}
252	}
253
254	#[pallet::call]
255	impl<T: Config> Pallet<T> {
256		/// Sends command to the Gateway contract to upgrade itself with a new implementation
257		/// contract
258		///
259		/// Fee required: No
260		///
261		/// - `origin`: Must be `Root`.
262		/// - `impl_address`: The address of the implementation contract.
263		/// - `impl_code_hash`: The codehash of the implementation contract.
264		/// - `initializer`: Optionally call an initializer on the implementation contract.
265		#[pallet::call_index(0)]
266		#[pallet::weight((T::WeightInfo::upgrade(), DispatchClass::Operational))]
267		pub fn upgrade(
268			origin: OriginFor<T>,
269			impl_address: H160,
270			impl_code_hash: H256,
271			initializer: Option<Initializer>,
272		) -> DispatchResult {
273			ensure_root(origin)?;
274
275			ensure!(
276				!impl_address.eq(&H160::zero()) && !impl_code_hash.eq(&H256::zero()),
277				Error::<T>::InvalidUpgradeParameters
278			);
279
280			let initializer_params_hash: Option<H256> =
281				initializer.as_ref().map(|i| H256::from(blake2_256(i.params.as_ref())));
282			let command = Command::Upgrade { impl_address, impl_code_hash, initializer };
283			Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
284
285			Self::deposit_event(Event::<T>::Upgrade {
286				impl_address,
287				impl_code_hash,
288				initializer_params_hash,
289			});
290			Ok(())
291		}
292
293		/// Sends a message to the Gateway contract to change its operating mode
294		///
295		/// Fee required: No
296		///
297		/// - `origin`: Must be `Location`
298		#[pallet::call_index(1)]
299		#[pallet::weight((T::WeightInfo::set_operating_mode(), DispatchClass::Operational))]
300		pub fn set_operating_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
301			ensure_root(origin)?;
302
303			let command = Command::SetOperatingMode { mode };
304			Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
305
306			Self::deposit_event(Event::<T>::SetOperatingMode { mode });
307			Ok(())
308		}
309
310		/// Set pricing parameters on both sides of the bridge
311		///
312		/// Fee required: No
313		///
314		/// - `origin`: Must be root
315		#[pallet::call_index(2)]
316		#[pallet::weight((T::WeightInfo::set_pricing_parameters(), DispatchClass::Operational))]
317		pub fn set_pricing_parameters(
318			origin: OriginFor<T>,
319			params: PricingParametersOf<T>,
320		) -> DispatchResult {
321			ensure_root(origin)?;
322			params.validate().map_err(|_| Error::<T>::InvalidPricingParameters)?;
323			PricingParameters::<T>::put(params.clone());
324
325			let command = Command::SetPricingParameters {
326				exchange_rate: params.exchange_rate.into(),
327				delivery_cost: T::InboundDeliveryCost::get().saturated_into::<u128>(),
328				multiplier: params.multiplier.into(),
329			};
330			Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
331
332			Self::deposit_event(Event::PricingParametersChanged { params });
333			Ok(())
334		}
335
336		/// Sends a message to the Gateway contract to update fee related parameters for
337		/// token transfers.
338		///
339		/// Privileged. Can only be called by root.
340		///
341		/// Fee required: No
342		///
343		/// - `origin`: Must be root
344		/// - `create_asset_xcm`: The XCM execution cost for creating a new asset class on AssetHub,
345		///   in DOT
346		/// - `transfer_asset_xcm`: The XCM execution cost for performing a reserve transfer on
347		///   AssetHub, in DOT
348		/// - `register_token`: The Ether fee for registering a new token, to discourage spamming
349		#[pallet::call_index(9)]
350		#[pallet::weight((T::WeightInfo::set_token_transfer_fees(), DispatchClass::Operational))]
351		pub fn set_token_transfer_fees(
352			origin: OriginFor<T>,
353			create_asset_xcm: u128,
354			transfer_asset_xcm: u128,
355			register_token: U256,
356		) -> DispatchResult {
357			ensure_root(origin)?;
358
359			// Basic validation of new costs. Particularly for token registration, we want to ensure
360			// its relatively expensive to discourage spamming. Like at least 100 USD.
361			ensure!(
362				create_asset_xcm > 0 && transfer_asset_xcm > 0 && register_token > meth(100),
363				Error::<T>::InvalidTokenTransferFees
364			);
365
366			let command = Command::SetTokenTransferFees {
367				create_asset_xcm,
368				transfer_asset_xcm,
369				register_token,
370			};
371			Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
372
373			Self::deposit_event(Event::<T>::SetTokenTransferFees {
374				create_asset_xcm,
375				transfer_asset_xcm,
376				register_token,
377			});
378			Ok(())
379		}
380
381		/// Registers a Polkadot-native token as a wrapped ERC20 token on Ethereum.
382		/// Privileged. Can only be called by root.
383		///
384		/// Fee required: No
385		///
386		/// - `origin`: Must be root
387		/// - `location`: Location of the asset (relative to this chain)
388		/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
389		#[pallet::call_index(10)]
390		#[pallet::weight(T::WeightInfo::register_token())]
391		pub fn register_token(
392			origin: OriginFor<T>,
393			location: Box<VersionedLocation>,
394			metadata: AssetMetadata,
395		) -> DispatchResultWithPostInfo {
396			ensure_root(origin)?;
397
398			let location: Location =
399				(*location).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
400
401			Self::do_register_token(&location, metadata, PaysFee::<T>::No)?;
402
403			Ok(PostDispatchInfo {
404				actual_weight: Some(T::WeightInfo::register_token()),
405				pays_fee: Pays::No,
406			})
407		}
408	}
409
410	impl<T: Config> Pallet<T> {
411		/// Send `command` to the Gateway on the Channel identified by `channel_id`
412		fn send(channel_id: ChannelId, command: Command, pays_fee: PaysFee<T>) -> DispatchResult {
413			let message = Message { id: None, channel_id, command };
414			let (ticket, fee) =
415				T::OutboundQueue::validate(&message).map_err(|err| Error::<T>::Send(err))?;
416
417			let payment = match pays_fee {
418				PaysFee::Yes(account) => Some((account, fee.total())),
419				PaysFee::Partial(account) => Some((account, fee.local)),
420				PaysFee::No => None,
421			};
422
423			if let Some((payer, fee)) = payment {
424				T::Token::transfer(
425					&payer,
426					&T::TreasuryAccount::get(),
427					fee,
428					Preservation::Preserve,
429				)?;
430			}
431
432			T::OutboundQueue::deliver(ticket).map_err(|err| Error::<T>::Send(err))?;
433			Ok(())
434		}
435
436		/// Initializes agents and channels.
437		pub fn initialize(para_id: ParaId, asset_hub_para_id: ParaId) -> Result<(), DispatchError> {
438			// Asset Hub
439			let asset_hub_location: Location =
440				ParentThen(Parachain(asset_hub_para_id.into()).into()).into();
441			let asset_hub_agent_id = agent_id_of::<T>(&asset_hub_location)?;
442			let asset_hub_channel_id: ChannelId = asset_hub_para_id.into();
443			Agents::<T>::insert(asset_hub_agent_id, ());
444			Channels::<T>::insert(
445				asset_hub_channel_id,
446				Channel { agent_id: asset_hub_agent_id, para_id: asset_hub_para_id },
447			);
448
449			// Governance channels
450			let bridge_hub_agent_id = agent_id_of::<T>(&Location::here())?;
451			// Agent for BridgeHub
452			Agents::<T>::insert(bridge_hub_agent_id, ());
453
454			// Primary governance channel
455			Channels::<T>::insert(
456				PRIMARY_GOVERNANCE_CHANNEL,
457				Channel { agent_id: bridge_hub_agent_id, para_id },
458			);
459
460			// Secondary governance channel
461			Channels::<T>::insert(
462				SECONDARY_GOVERNANCE_CHANNEL,
463				Channel { agent_id: bridge_hub_agent_id, para_id },
464			);
465
466			Ok(())
467		}
468
469		/// Checks if the pallet has been initialized.
470		pub(crate) fn is_initialized() -> bool {
471			let primary_exists = Channels::<T>::contains_key(PRIMARY_GOVERNANCE_CHANNEL);
472			let secondary_exists = Channels::<T>::contains_key(SECONDARY_GOVERNANCE_CHANNEL);
473			primary_exists && secondary_exists
474		}
475
476		pub(crate) fn do_register_token(
477			location: &Location,
478			metadata: AssetMetadata,
479			pays_fee: PaysFee<T>,
480		) -> Result<(), DispatchError> {
481			let ethereum_location = T::EthereumLocation::get();
482			// reanchor to Ethereum context
483			let location = location
484				.clone()
485				.reanchored(&ethereum_location, &T::UniversalLocation::get())
486				.map_err(|_| Error::<T>::LocationConversionFailed)?;
487
488			let token_id = TokenIdOf::convert_location(&location)
489				.ok_or(Error::<T>::LocationConversionFailed)?;
490
491			if !ForeignToNativeId::<T>::contains_key(token_id) {
492				ForeignToNativeId::<T>::insert(token_id, location.clone());
493			}
494
495			let command = Command::RegisterForeignToken {
496				token_id,
497				name: metadata.name.into_inner(),
498				symbol: metadata.symbol.into_inner(),
499				decimals: metadata.decimals,
500			};
501			Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;
502
503			Self::deposit_event(Event::<T>::RegisterToken {
504				location: location.clone().into(),
505				foreign_token_id: token_id,
506			});
507
508			Ok(())
509		}
510	}
511
512	impl<T: Config> StaticLookup for Pallet<T> {
513		type Source = ChannelId;
514		type Target = Channel;
515		fn lookup(channel_id: Self::Source) -> Option<Self::Target> {
516			Channels::<T>::get(channel_id)
517		}
518	}
519
520	impl<T: Config> Contains<ChannelId> for Pallet<T> {
521		fn contains(channel_id: &ChannelId) -> bool {
522			Channels::<T>::get(channel_id).is_some()
523		}
524	}
525
526	impl<T: Config> Get<PricingParametersOf<T>> for Pallet<T> {
527		fn get() -> PricingParametersOf<T> {
528			PricingParameters::<T>::get()
529		}
530	}
531
532	impl<T: Config> MaybeConvert<TokenId, Location> for Pallet<T> {
533		fn maybe_convert(foreign_id: TokenId) -> Option<Location> {
534			ForeignToNativeId::<T>::get(foreign_id)
535		}
536	}
537}