referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_system_v2/
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//! * [`Call::upgrade`]: Upgrade the Gateway contract on Ethereum.
10//! * [`Call::set_operating_mode`]: Set the operating mode of the Gateway contract
11//!
12//! ## Polkadot-native tokens on Ethereum
13//!
14//! Tokens deposited on AssetHub pallet can be bridged to Ethereum as wrapped ERC20 tokens. As a
15//! prerequisite, the token should be registered first.
16//!
17//! * [`Call::register_token`]: Register a token location as a wrapped ERC20 contract on Ethereum.
18#![cfg_attr(not(feature = "std"), no_std)]
19#[cfg(test)]
20mod mock;
21
22#[cfg(test)]
23mod tests;
24
25#[cfg(feature = "runtime-benchmarks")]
26mod benchmarking;
27
28pub mod api;
29pub mod weights;
30pub use weights::*;
31
32use frame_support::{pallet_prelude::*, traits::EnsureOrigin};
33use frame_system::pallet_prelude::*;
34pub use pallet::*;
35use snowbridge_core::{
36	reward::{
37		AddTip, MessageId,
38		MessageId::{Inbound, Outbound},
39	},
40	AgentIdOf as LocationHashOf, AssetMetadata, TokenId, TokenIdOf,
41};
42use snowbridge_outbound_queue_primitives::{
43	v2::{Command, Initializer, Message, SendMessage},
44	OperatingMode, SendError,
45};
46use snowbridge_pallet_system::ForeignToNativeId;
47use sp_core::{H160, H256};
48use sp_io::hashing::blake2_256;
49use sp_runtime::traits::MaybeConvert;
50use sp_std::prelude::*;
51use xcm::prelude::*;
52use xcm_executor::traits::ConvertLocation;
53
54#[cfg(feature = "runtime-benchmarks")]
55use frame_support::traits::OriginTrait;
56
57pub const LOG_TARGET: &str = "snowbridge-system-v2";
58
59pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
60#[cfg(feature = "runtime-benchmarks")]
61pub trait BenchmarkHelper<O>
62where
63	O: OriginTrait,
64{
65	fn make_xcm_origin(location: Location) -> O;
66}
67
68#[frame_support::pallet]
69pub mod pallet {
70	use super::*;
71
72	#[pallet::pallet]
73	pub struct Pallet<T>(_);
74
75	#[pallet::config]
76	pub trait Config: frame_system::Config + snowbridge_pallet_system::Config {
77		#[allow(deprecated)]
78		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
79		/// Send messages to Ethereum and add additional relayer rewards if deposited
80		type OutboundQueue: SendMessage + AddTip;
81		/// Add to the relayer reward for a specific message
82		type InboundQueue: AddTip;
83		/// Origin check for XCM locations that transact with this pallet
84		type FrontendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Location>;
85		/// Origin for governance calls
86		type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Location>;
87		type WeightInfo: WeightInfo;
88		#[cfg(feature = "runtime-benchmarks")]
89		type Helper: BenchmarkHelper<Self::RuntimeOrigin>;
90	}
91
92	#[pallet::event]
93	#[pallet::generate_deposit(pub(super) fn deposit_event)]
94	pub enum Event<T: Config> {
95		/// An Upgrade message was sent to the Gateway
96		Upgrade { impl_address: H160, impl_code_hash: H256, initializer_params_hash: H256 },
97		/// An SetOperatingMode message was sent to the Gateway
98		SetOperatingMode { mode: OperatingMode },
99		/// Register Polkadot-native token as a wrapped ERC20 token on Ethereum
100		RegisterToken {
101			/// Location of Polkadot-native token
102			location: VersionedLocation,
103			/// ID of Polkadot-native token on Ethereum
104			foreign_token_id: H256,
105		},
106		/// A tip was processed for an inbound or outbound message, for relayer incentivization.
107		/// It could have succeeded or failed (and then added to LostTips).
108		TipProcessed {
109			/// The original sender of the tip (who deposited the funds).
110			sender: AccountIdOf<T>,
111			/// The Inbound/Outbound message nonce
112			message_id: MessageId,
113			/// The tip amount in ether.
114			amount: u128,
115			/// Whether the tip was added successfully. If the tip was added for a nonce
116			/// that was already consumed, the tip will be added to LostTips.
117			success: bool,
118		},
119	}
120
121	#[pallet::error]
122	pub enum Error<T> {
123		/// Location could not be reachored
124		LocationReanchorFailed,
125		/// A token location could not be converted to a token ID.
126		LocationConversionFailed,
127		/// A `VersionedLocation` could not be converted into a `Location`.
128		UnsupportedLocationVersion,
129		/// An XCM could not be sent, due to a `SendError`.
130		Send(SendError),
131		/// The gateway contract upgrade message could not be sent due to invalid upgrade
132		/// parameters.
133		InvalidUpgradeParameters,
134	}
135
136	/// Relayer reward tips that were paid by the user to incentivize the processing of their
137	/// message, but then could not be added to their message reward (e.g. the nonce was already
138	/// processed or their order could not be found). Capturing the lost tips here supports
139	/// implementing a recovery method in the future.
140	#[pallet::storage]
141	pub type LostTips<T: Config> =
142		StorageMap<_, Blake2_128Concat, AccountIdOf<T>, u128, ValueQuery>;
143
144	#[pallet::call]
145	impl<T: Config> Pallet<T> {
146		/// Sends command to the Gateway contract to upgrade itself with a new implementation
147		/// contract
148		///
149		/// Fee required: No
150		///
151		/// - `origin`: Must be `Root`.
152		/// - `impl_address`: The address of the implementation contract.
153		/// - `impl_code_hash`: The codehash of the implementation contract.
154		/// - `initializer`: Optionally call an initializer on the implementation contract.
155		#[pallet::call_index(0)]
156		#[pallet::weight((<T as pallet::Config>::WeightInfo::upgrade(), DispatchClass::Operational))]
157		pub fn upgrade(
158			origin: OriginFor<T>,
159			impl_address: H160,
160			impl_code_hash: H256,
161			initializer: Initializer,
162		) -> DispatchResult {
163			let origin_location = T::GovernanceOrigin::ensure_origin(origin)?;
164			let origin = Self::location_to_message_origin(origin_location)?;
165
166			ensure!(
167				!impl_address.eq(&H160::zero()) && !impl_code_hash.eq(&H256::zero()),
168				Error::<T>::InvalidUpgradeParameters
169			);
170
171			let initializer_params_hash: H256 = blake2_256(initializer.params.as_ref()).into();
172
173			let command = Command::Upgrade { impl_address, impl_code_hash, initializer };
174			Self::send(origin, command, 0)?;
175
176			Self::deposit_event(Event::<T>::Upgrade {
177				impl_address,
178				impl_code_hash,
179				initializer_params_hash,
180			});
181			Ok(())
182		}
183
184		/// Sends a message to the Gateway contract to change its operating mode
185		///
186		/// Fee required: No
187		///
188		/// - `origin`: Must be `GovernanceOrigin`
189		#[pallet::call_index(1)]
190		#[pallet::weight((<T as pallet::Config>::WeightInfo::set_operating_mode(), DispatchClass::Operational))]
191		pub fn set_operating_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
192			let origin_location = T::GovernanceOrigin::ensure_origin(origin)?;
193			let origin = Self::location_to_message_origin(origin_location)?;
194
195			let command = Command::SetOperatingMode { mode };
196			Self::send(origin, command, 0)?;
197
198			Self::deposit_event(Event::<T>::SetOperatingMode { mode });
199			Ok(())
200		}
201
202		/// Registers a Polkadot-native token as a wrapped ERC20 token on Ethereum.
203		///
204		/// The system frontend pallet on AH proxies this call to BH.
205		///
206		/// - `sender`: The original sender initiating the call on AH
207		/// - `asset_id`: Location of the asset (relative to this chain)
208		/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
209		#[pallet::call_index(2)]
210		#[pallet::weight(<T as pallet::Config>::WeightInfo::register_token())]
211		pub fn register_token(
212			origin: OriginFor<T>,
213			sender: Box<VersionedLocation>,
214			asset_id: Box<VersionedLocation>,
215			metadata: AssetMetadata,
216			amount: u128,
217		) -> DispatchResult {
218			T::FrontendOrigin::ensure_origin(origin)?;
219
220			let sender_location: Location =
221				(*sender).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
222			let asset_location: Location =
223				(*asset_id).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
224
225			let location = Self::reanchor(asset_location)?;
226			let token_id = TokenIdOf::convert_location(&location)
227				.ok_or(Error::<T>::LocationConversionFailed)?;
228
229			if !ForeignToNativeId::<T>::contains_key(token_id) {
230				ForeignToNativeId::<T>::insert(token_id, location.clone());
231			}
232
233			let command = Command::RegisterForeignToken {
234				token_id,
235				name: metadata.name.into_inner(),
236				symbol: metadata.symbol.into_inner(),
237				decimals: metadata.decimals,
238			};
239
240			let message_origin = Self::location_to_message_origin(sender_location)?;
241			Self::send(message_origin, command, amount)?;
242
243			Self::deposit_event(Event::<T>::RegisterToken {
244				location: location.into(),
245				foreign_token_id: token_id,
246			});
247
248			Ok(())
249		}
250
251		#[pallet::call_index(3)]
252		#[pallet::weight(<T as pallet::Config>::WeightInfo::add_tip())]
253		pub fn add_tip(
254			origin: OriginFor<T>,
255			sender: AccountIdOf<T>,
256			message_id: MessageId,
257			amount: u128,
258		) -> DispatchResult {
259			T::FrontendOrigin::ensure_origin(origin)?;
260
261			let result = match message_id {
262				Inbound(nonce) => <T as pallet::Config>::InboundQueue::add_tip(nonce, amount),
263				Outbound(nonce) => <T as pallet::Config>::OutboundQueue::add_tip(nonce, amount),
264			};
265
266			if let Err(ref e) = result {
267				tracing::debug!(target: LOG_TARGET, ?e, ?message_id, ?amount, "error adding tip");
268				LostTips::<T>::mutate(&sender, |lost_tip| {
269					*lost_tip = lost_tip.saturating_add(amount);
270				});
271			}
272
273			Self::deposit_event(Event::<T>::TipProcessed {
274				sender,
275				message_id,
276				amount,
277				success: result.is_ok(),
278			});
279
280			Ok(())
281		}
282	}
283
284	impl<T: Config> Pallet<T> {
285		/// Send `command` to the Gateway from a specific origin/agent
286		fn send(origin: H256, command: Command, fee: u128) -> DispatchResult {
287			let message = Message {
288				origin,
289				id: frame_system::unique((origin, &command, fee)).into(),
290				fee,
291				commands: BoundedVec::try_from(vec![command]).unwrap(),
292			};
293
294			let ticket = <T as pallet::Config>::OutboundQueue::validate(&message)
295				.map_err(|err| Error::<T>::Send(err))?;
296
297			<T as pallet::Config>::OutboundQueue::deliver(ticket)
298				.map_err(|err| Error::<T>::Send(err))?;
299			Ok(())
300		}
301
302		/// Reanchor the `location` in context of ethereum
303		pub fn reanchor(location: Location) -> Result<Location, Error<T>> {
304			location
305				.reanchored(&T::EthereumLocation::get(), &T::UniversalLocation::get())
306				.map_err(|_| Error::<T>::LocationReanchorFailed)
307		}
308
309		pub fn location_to_message_origin(location: Location) -> Result<H256, Error<T>> {
310			let reanchored_location = Self::reanchor(location)?;
311			LocationHashOf::convert_location(&reanchored_location)
312				.ok_or(Error::<T>::LocationConversionFailed)
313		}
314	}
315
316	impl<T: Config> MaybeConvert<TokenId, Location> for Pallet<T> {
317		fn maybe_convert(foreign_id: TokenId) -> Option<Location> {
318			snowbridge_pallet_system::Pallet::<T>::maybe_convert(foreign_id)
319		}
320	}
321}