referrerpolicy=no-referrer-when-downgrade

snowbridge_core/
reward.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3
4extern crate alloc;
5
6use crate::reward::RewardPaymentError::{ChargeFeesFailure, XcmSendFailure};
7use bp_relayers::PaymentProcedure;
8use codec::DecodeWithMemTracking;
9use frame_support::{dispatch::GetDispatchInfo, PalletError};
10use scale_info::TypeInfo;
11use sp_runtime::{
12	codec::{Decode, Encode},
13	traits::Get,
14	DispatchError,
15};
16use sp_std::{fmt::Debug, marker::PhantomData};
17use xcm::{
18	opaque::latest::prelude::Xcm,
19	prelude::{ExecuteXcm, Junction::*, Location, SendXcm, *},
20};
21
22/// Describes the message that the tip should be added to (either Inbound or Outbound message) and
23/// the message nonce.
24#[derive(Debug, Clone, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo)]
25pub enum MessageId {
26	/// Message from Ethereum
27	Inbound(u64),
28	/// Message to Ethereum
29	Outbound(u64),
30}
31
32#[derive(Debug, Encode, PartialEq, DecodeWithMemTracking, Decode, TypeInfo, PalletError)]
33pub enum AddTipError {
34	NonceConsumed,
35	UnknownMessage,
36	AmountZero,
37}
38
39/// Trait to add a tip for a nonce.
40pub trait AddTip {
41	/// Add a relayer reward tip to a pallet.
42	fn add_tip(nonce: u64, amount: u128) -> Result<(), AddTipError>;
43}
44
45/// Error related to paying out relayer rewards.
46#[derive(Debug, Encode, Decode)]
47pub enum RewardPaymentError {
48	/// The XCM to mint the reward on AssetHub could not be sent.
49	XcmSendFailure,
50	/// The delivery fee to send the XCM could not be charged.
51	ChargeFeesFailure,
52}
53
54impl From<RewardPaymentError> for DispatchError {
55	fn from(e: RewardPaymentError) -> DispatchError {
56		match e {
57			XcmSendFailure => DispatchError::Other("xcm send failure"),
58			ChargeFeesFailure => DispatchError::Other("charge fees error"),
59		}
60	}
61}
62
63/// Reward payment procedure that sends a XCM to AssetHub to mint the reward (foreign asset)
64/// into the provided beneficiary account.
65pub struct PayAccountOnLocation<
66	Relayer,
67	RewardBalance,
68	EthereumNetwork,
69	AssetHubLocation,
70	InboundQueueLocation,
71	XcmSender,
72	XcmExecutor,
73	Call,
74>(
75	PhantomData<(
76		Relayer,
77		RewardBalance,
78		EthereumNetwork,
79		AssetHubLocation,
80		InboundQueueLocation,
81		XcmSender,
82		XcmExecutor,
83		Call,
84	)>,
85);
86
87impl<
88		Relayer,
89		RewardBalance,
90		EthereumNetwork,
91		AssetHubLocation,
92		InboundQueueLocation,
93		XcmSender,
94		XcmExecutor,
95		Call,
96	> PaymentProcedure<Relayer, (), RewardBalance>
97	for PayAccountOnLocation<
98		Relayer,
99		RewardBalance,
100		EthereumNetwork,
101		AssetHubLocation,
102		InboundQueueLocation,
103		XcmSender,
104		XcmExecutor,
105		Call,
106	>
107where
108	Relayer: Clone
109		+ Debug
110		+ Decode
111		+ Encode
112		+ Eq
113		+ TypeInfo
114		+ Into<sp_runtime::AccountId32>
115		+ Into<Location>,
116	EthereumNetwork: Get<NetworkId>,
117	InboundQueueLocation: Get<InteriorLocation>,
118	AssetHubLocation: Get<Location>,
119	XcmSender: SendXcm,
120	RewardBalance: Into<u128> + Clone,
121	XcmExecutor: ExecuteXcm<Call>,
122	Call: Decode + GetDispatchInfo,
123{
124	type Error = DispatchError;
125	type Beneficiary = Location;
126
127	fn pay_reward(
128		relayer: &Relayer,
129		_: (),
130		reward: RewardBalance,
131		beneficiary: Self::Beneficiary,
132	) -> Result<(), Self::Error> {
133		let ethereum_location = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
134		let assets: Asset = (ethereum_location.clone(), reward.into()).into();
135
136		let xcm: Xcm<()> = alloc::vec![
137			UnpaidExecution { weight_limit: Unlimited, check_origin: None },
138			DescendOrigin(InboundQueueLocation::get().into()),
139			UniversalOrigin(GlobalConsensus(EthereumNetwork::get())),
140			ReserveAssetDeposited(assets.into()),
141			DepositAsset { assets: AllCounted(1).into(), beneficiary },
142		]
143		.into();
144
145		let (ticket, fee) =
146			validate_send::<XcmSender>(AssetHubLocation::get(), xcm).map_err(|_| XcmSendFailure)?;
147		XcmExecutor::charge_fees(relayer.clone(), fee).map_err(|_| ChargeFeesFailure)?;
148		XcmSender::deliver(ticket).map_err(|_| XcmSendFailure)?;
149
150		Ok(())
151	}
152}
153
154#[cfg(test)]
155mod tests {
156	use super::*;
157	use frame_support::parameter_types;
158	use sp_runtime::AccountId32;
159
160	#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)]
161	pub struct MockRelayer(pub AccountId32);
162
163	impl From<MockRelayer> for AccountId32 {
164		fn from(m: MockRelayer) -> Self {
165			m.0
166		}
167	}
168
169	impl From<MockRelayer> for Location {
170		fn from(_m: MockRelayer) -> Self {
171			// For simplicity, return a dummy location
172			Location::new(1, Here)
173		}
174	}
175
176	pub enum BridgeReward {
177		Snowbridge,
178	}
179
180	parameter_types! {
181		pub AssetHubLocation: Location = Location::new(1,[Parachain(1000)]);
182		pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into();
183		pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
184		pub const DefaultMyRewardKind: BridgeReward = BridgeReward::Snowbridge;
185	}
186
187	pub enum Weightless {}
188	impl PreparedMessage for Weightless {
189		fn weight_of(&self) -> Weight {
190			unreachable!();
191		}
192	}
193
194	pub struct MockXcmExecutor;
195	impl<C> ExecuteXcm<C> for MockXcmExecutor {
196		type Prepared = Weightless;
197		fn prepare(_: Xcm<C>, _: Weight) -> Result<Self::Prepared, InstructionError> {
198			Err(InstructionError { index: 0, error: XcmError::Unimplemented })
199		}
200		fn execute(
201			_: impl Into<Location>,
202			_: Self::Prepared,
203			_: &mut XcmHash,
204			_: Weight,
205		) -> Outcome {
206			unreachable!()
207		}
208		fn charge_fees(_: impl Into<Location>, _: Assets) -> xcm::latest::Result {
209			Ok(())
210		}
211	}
212
213	#[derive(Debug, Decode, Default)]
214	pub struct MockCall;
215	impl GetDispatchInfo for MockCall {
216		fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo {
217			Default::default()
218		}
219	}
220
221	pub struct MockXcmSender;
222	impl SendXcm for MockXcmSender {
223		type Ticket = Xcm<()>;
224
225		fn validate(
226			dest: &mut Option<Location>,
227			xcm: &mut Option<Xcm<()>>,
228		) -> SendResult<Self::Ticket> {
229			if let Some(location) = dest {
230				match location.unpack() {
231					(_, [Parachain(1001)]) => return Err(SendError::NotApplicable),
232					_ => Ok((xcm.clone().unwrap(), Assets::default())),
233				}
234			} else {
235				Ok((xcm.clone().unwrap(), Assets::default()))
236			}
237		}
238
239		fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
240			let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
241			Ok(hash)
242		}
243	}
244
245	#[test]
246	fn pay_reward_success() {
247		let relayer = MockRelayer(AccountId32::new([1u8; 32]));
248		let beneficiary = Location::new(1, Here);
249		let reward = 1_000u128;
250
251		type TestedPayAccountOnLocation = PayAccountOnLocation<
252			MockRelayer,
253			u128,
254			EthereumNetwork,
255			AssetHubLocation,
256			InboundQueueLocation,
257			MockXcmSender,
258			MockXcmExecutor,
259			MockCall,
260		>;
261
262		let result = TestedPayAccountOnLocation::pay_reward(&relayer, (), reward, beneficiary);
263
264		assert!(result.is_ok());
265	}
266
267	#[test]
268	fn pay_reward_fails_on_xcm_validate_xcm() {
269		struct FailingXcmValidator;
270		impl SendXcm for FailingXcmValidator {
271			type Ticket = ();
272
273			fn validate(
274				_dest: &mut Option<Location>,
275				_xcm: &mut Option<Xcm<()>>,
276			) -> SendResult<Self::Ticket> {
277				Err(SendError::NotApplicable)
278			}
279
280			fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
281				let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
282				Ok(hash)
283			}
284		}
285
286		type FailingSenderPayAccount = PayAccountOnLocation<
287			MockRelayer,
288			u128,
289			EthereumNetwork,
290			AssetHubLocation,
291			InboundQueueLocation,
292			FailingXcmValidator,
293			MockXcmExecutor,
294			MockCall,
295		>;
296
297		let relayer = MockRelayer(AccountId32::new([1u8; 32]));
298		let reward = 1_000u128;
299		let beneficiary = Location::new(1, Here);
300		let result = FailingSenderPayAccount::pay_reward(&relayer, (), reward, beneficiary);
301
302		assert!(result.is_err());
303		let err_str = format!("{:?}", result.err().unwrap());
304		assert!(
305			err_str.contains("xcm send failure"),
306			"Expected xcm send failure error, got {:?}",
307			err_str
308		);
309	}
310
311	#[test]
312	fn pay_reward_fails_on_charge_fees() {
313		struct FailingXcmExecutor;
314		impl<C> ExecuteXcm<C> for FailingXcmExecutor {
315			type Prepared = Weightless;
316			fn prepare(_: Xcm<C>, _: Weight) -> Result<Self::Prepared, InstructionError> {
317				Err(InstructionError { index: 0, error: XcmError::Unimplemented })
318			}
319			fn execute(
320				_: impl Into<Location>,
321				_: Self::Prepared,
322				_: &mut XcmHash,
323				_: Weight,
324			) -> Outcome {
325				unreachable!()
326			}
327			fn charge_fees(_: impl Into<Location>, _: Assets) -> xcm::latest::Result {
328				Err(crate::reward::SendError::Fees.into())
329			}
330		}
331
332		type FailingExecutorPayAccount = PayAccountOnLocation<
333			MockRelayer,
334			u128,
335			EthereumNetwork,
336			AssetHubLocation,
337			InboundQueueLocation,
338			MockXcmSender,
339			FailingXcmExecutor,
340			MockCall,
341		>;
342
343		let relayer = MockRelayer(AccountId32::new([3u8; 32]));
344		let beneficiary = Location::new(1, Here);
345		let reward = 500u128;
346		let result = FailingExecutorPayAccount::pay_reward(&relayer, (), reward, beneficiary);
347
348		assert!(result.is_err());
349		let err_str = format!("{:?}", result.err().unwrap());
350		assert!(
351			err_str.contains("charge fees error"),
352			"Expected 'charge fees error', got {:?}",
353			err_str
354		);
355	}
356
357	#[test]
358	fn pay_reward_fails_on_delivery() {
359		#[derive(Default)]
360		struct FailingDeliveryXcmSender;
361		impl SendXcm for FailingDeliveryXcmSender {
362			type Ticket = ();
363
364			fn validate(
365				_dest: &mut Option<Location>,
366				_xcm: &mut Option<Xcm<()>>,
367			) -> SendResult<Self::Ticket> {
368				Ok(((), Assets::from(vec![])))
369			}
370
371			fn deliver(_xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
372				Err(SendError::NotApplicable)
373			}
374		}
375
376		type FailingDeliveryPayAccount = PayAccountOnLocation<
377			MockRelayer,
378			u128,
379			EthereumNetwork,
380			AssetHubLocation,
381			InboundQueueLocation,
382			FailingDeliveryXcmSender,
383			MockXcmExecutor,
384			MockCall,
385		>;
386
387		let relayer = MockRelayer(AccountId32::new([4u8; 32]));
388		let beneficiary = Location::new(1, Here);
389		let reward = 123u128;
390		let result = FailingDeliveryPayAccount::pay_reward(&relayer, (), reward, beneficiary);
391
392		assert!(result.is_err());
393		let err_str = format!("{:?}", result.err().unwrap());
394		assert!(
395			err_str.contains("xcm send failure"),
396			"Expected 'xcm delivery failure', got {:?}",
397			err_str
398		);
399	}
400}