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