referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_inbound_queue_v2/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Inbound Queue
//!
//! # Overview
//!
//! Receives messages emitted by the Gateway contract on Ethereum, whereupon they are verified,
//! translated to XCM, and finally sent to AssetHub for further processing.
//!
//! Message relayers are rewarded in wrapped Ether that is included within the message. This
//! wrapped Ether is derived from Ether that the message origin has locked up on Ethereum.
//!
//! # Extrinsics
//!
//! ## Governance
//!
//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
//!   processing of inbound messages.
//!
//! ## Message Submission
//!
//! * [`Call::submit`]: Submit a message for verification and dispatch to the final destination
//!   parachain.
#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod test;

pub use crate::weights::WeightInfo;
use frame_support::{
	traits::{
		fungible::{Inspect, Mutate},
		tokens::Balance,
	},
	weights::WeightToFee,
};
use frame_system::ensure_signed;
use snowbridge_core::{
	sparse_bitmap::{SparseBitmap, SparseBitmapImpl},
	BasicOperatingMode,
};
use snowbridge_inbound_queue_primitives::{
	v2::{ConvertMessage, ConvertMessageError, Message},
	EventProof, VerificationError, Verifier,
};
use sp_core::H160;
use sp_runtime::traits::TryConvert;
use sp_std::prelude::*;
use xcm::prelude::{ExecuteXcm, Junction::*, Location, SendXcm, *};

use bp_relayers::RewardLedger;
#[cfg(feature = "runtime-benchmarks")]
use {snowbridge_beacon_primitives::BeaconHeader, sp_core::H256};

pub use pallet::*;

pub const LOG_TARGET: &str = "snowbridge-pallet-inbound-queue-v2";

pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
type BalanceOf<T> =
	<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;

pub type Nonce<T> = SparseBitmapImpl<crate::NonceBitmap<T>>;

#[frame_support::pallet]
pub mod pallet {
	use super::*;

	use frame_support::pallet_prelude::*;
	use frame_system::pallet_prelude::*;

	#[pallet::pallet]
	pub struct Pallet<T>(_);

	#[cfg(feature = "runtime-benchmarks")]
	pub trait BenchmarkHelper<T> {
		fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256);
	}

	#[pallet::config]
	pub trait Config: frame_system::Config {
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
		/// The verifier for inbound messages from Ethereum.
		type Verifier: Verifier;
		/// XCM message sender.
		type XcmSender: SendXcm;
		/// Handler for XCM fees.
		type XcmExecutor: ExecuteXcm<Self::RuntimeCall>;
		/// Ethereum NetworkId
		type EthereumNetwork: Get<NetworkId>;
		/// Address of the Gateway contract.
		#[pallet::constant]
		type GatewayAddress: Get<H160>;
		/// AssetHub parachain ID.
		type AssetHubParaId: Get<u32>;
		/// Convert a command from Ethereum to an XCM message.
		type MessageConverter: ConvertMessage;
		#[cfg(feature = "runtime-benchmarks")]
		type Helper: BenchmarkHelper<Self>;
		/// Used for the dry run API implementation.
		type Balance: Balance + From<u128>;
		/// Reward discriminator type.
		type RewardKind: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone;
		/// The default RewardKind discriminator for rewards allocated to relayers from this pallet.
		#[pallet::constant]
		type DefaultRewardKind: Get<Self::RewardKind>;
		/// Relayer reward payment.
		type RewardPayment: RewardLedger<Self::AccountId, Self::RewardKind, u128>;
		type WeightInfo: WeightInfo;
		/// Convert a weight value into deductible balance type.
		type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
		type Token: Mutate<Self::AccountId> + Inspect<Self::AccountId>;
		/// AccountId to Location converter
		type AccountToLocation: for<'a> TryConvert<&'a Self::AccountId, Location>;
	}

	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		/// A message was received from Ethereum
		MessageReceived {
			/// The message nonce
			nonce: u64,
			/// ID of the XCM message which was forwarded to the final destination parachain
			message_id: [u8; 32],
		},
		/// Set OperatingMode
		OperatingModeChanged { mode: BasicOperatingMode },
		/// XCM delivery fees were paid.
		FeesPaid { paying: Location, fees: Assets },
	}

	#[pallet::error]
	pub enum Error<T> {
		/// Message came from an invalid outbound channel on the Ethereum side.
		InvalidGateway,
		/// Account could not be converted to bytes
		InvalidAccount,
		/// Message has an invalid envelope.
		InvalidMessage,
		/// Message has an unexpected nonce.
		InvalidNonce,
		/// Fee provided is invalid.
		InvalidFee,
		/// Message has an invalid payload.
		InvalidPayload,
		/// Message channel is invalid
		InvalidChannel,
		/// The max nonce for the type has been reached
		MaxNonceReached,
		/// Cannot convert location
		InvalidAccountConversion,
		/// Invalid network specified
		InvalidNetwork,
		/// Pallet is halted
		Halted,
		/// The operation required fees to be paid which the initiator could not meet.
		FeesNotMet,
		/// The desired destination was unreachable, generally because there is a no way of routing
		/// to it.
		Unreachable,
		/// There was some other issue (i.e. not to do with routing) in sending the message.
		/// Perhaps a lack of space for buffering the message.
		SendFailure,
		/// Invalid foreign ERC-20 token ID
		InvalidAsset,
		/// Cannot reachor a foreign ERC-20 asset location.
		CannotReanchor,
		/// Message verification error
		Verification(VerificationError),
	}

	impl<T: Config> From<SendError> for Error<T> {
		fn from(e: SendError) -> Self {
			match e {
				SendError::Fees => Error::<T>::FeesNotMet,
				SendError::NotApplicable => Error::<T>::Unreachable,
				_ => Error::<T>::SendFailure,
			}
		}
	}

	impl<T: Config> From<ConvertMessageError> for Error<T> {
		fn from(e: ConvertMessageError) -> Self {
			match e {
				ConvertMessageError::InvalidAsset => Error::<T>::InvalidAsset,
				ConvertMessageError::CannotReanchor => Error::<T>::CannotReanchor,
				ConvertMessageError::InvalidNetwork => Error::<T>::InvalidNetwork,
			}
		}
	}

	/// StorageMap used for encoding a SparseBitmapImpl that tracks whether a specific nonce has
	/// been processed or not. Message nonces are unique and never repeated.
	#[pallet::storage]
	pub type NonceBitmap<T: Config> = StorageMap<_, Twox64Concat, u128, u128, ValueQuery>;

	/// The current operating mode of the pallet.
	#[pallet::storage]
	pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;

	#[pallet::call]
	impl<T: Config> Pallet<T> {
		/// Submit an inbound message originating from the Gateway contract on Ethereum
		#[pallet::call_index(0)]
		#[pallet::weight(T::WeightInfo::submit())]
		pub fn submit(origin: OriginFor<T>, event: Box<EventProof>) -> DispatchResult {
			let who = ensure_signed(origin)?;
			ensure!(!OperatingMode::<T>::get().is_halted(), Error::<T>::Halted);

			// submit message for verification
			T::Verifier::verify(&event.event_log, &event.proof)
				.map_err(|e| Error::<T>::Verification(e))?;

			// Decode event log into a bridge message
			let message =
				Message::try_from(&event.event_log).map_err(|_| Error::<T>::InvalidMessage)?;

			Self::process_message(who, message)
		}

		/// Halt or resume all pallet operations. May only be called by root.
		#[pallet::call_index(1)]
		#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
		pub fn set_operating_mode(
			origin: OriginFor<T>,
			mode: BasicOperatingMode,
		) -> DispatchResult {
			ensure_root(origin)?;
			OperatingMode::<T>::set(mode);
			Self::deposit_event(Event::OperatingModeChanged { mode });
			Ok(())
		}
	}

	impl<T: Config> Pallet<T> {
		pub fn process_message(relayer: T::AccountId, message: Message) -> DispatchResult {
			// Verify that the message was submitted from the known Gateway contract
			ensure!(T::GatewayAddress::get() == message.gateway, Error::<T>::InvalidGateway);

			let (nonce, relayer_fee) = (message.nonce, message.relayer_fee);

			// Verify the message has not been processed
			ensure!(!Nonce::<T>::get(nonce.into()), Error::<T>::InvalidNonce);

			let xcm =
				T::MessageConverter::convert(message).map_err(|error| Error::<T>::from(error))?;

			// Forward XCM to AH
			let dest = Location::new(1, [Parachain(T::AssetHubParaId::get())]);
			let message_id =
				Self::send_xcm(dest.clone(), &relayer, xcm.clone()).map_err(|error| {
					tracing::error!(target: LOG_TARGET, ?error, ?dest, ?xcm, "XCM send failed with error");
					Error::<T>::from(error)
				})?;

			// Pay relayer reward
			T::RewardPayment::register_reward(&relayer, T::DefaultRewardKind::get(), relayer_fee);

			// Mark message as received
			Nonce::<T>::set(nonce.into());

			Self::deposit_event(Event::MessageReceived { nonce, message_id });

			Ok(())
		}

		fn send_xcm(
			dest: Location,
			fee_payer: &T::AccountId,
			xcm: Xcm<()>,
		) -> Result<XcmHash, SendError> {
			let (ticket, fee) = validate_send::<T::XcmSender>(dest, xcm)?;
			let fee_payer = T::AccountToLocation::try_convert(fee_payer).map_err(|err| {
				tracing::error!(
					target: LOG_TARGET,
					?err,
					"Failed to convert account to XCM location",
				);
				SendError::NotApplicable
			})?;
			T::XcmExecutor::charge_fees(fee_payer.clone(), fee.clone()).map_err(|error| {
				tracing::error!(
					target: LOG_TARGET,
					?error,
					"Charging fees failed with error",
				);
				SendError::Fees
			})?;
			Self::deposit_event(Event::FeesPaid { paying: fee_payer, fees: fee });
			T::XcmSender::deliver(ticket)
		}
	}
}