Skip to main content

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	#[allow(dead_code)]
177	pub enum BridgeReward {
178		Snowbridge,
179	}
180
181	parameter_types! {
182		pub AssetHubLocation: Location = Location::new(1,[Parachain(1000)]);
183		pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into();
184		pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
185		pub const DefaultMyRewardKind: BridgeReward = BridgeReward::Snowbridge;
186	}
187
188	pub enum Weightless {}
189	impl PreparedMessage for Weightless {
190		fn weight_of(&self) -> Weight {
191			unreachable!();
192		}
193	}
194
195	pub struct MockXcmExecutor;
196	impl<C> ExecuteXcm<C> for MockXcmExecutor {
197		type Prepared = Weightless;
198		fn prepare(_: Xcm<C>, _: Weight) -> Result<Self::Prepared, InstructionError> {
199			Err(InstructionError { index: 0, error: XcmError::Unimplemented })
200		}
201		fn execute(
202			_: impl Into<Location>,
203			_: Self::Prepared,
204			_: &mut XcmHash,
205			_: Weight,
206		) -> Outcome {
207			unreachable!()
208		}
209		fn charge_fees(_: impl Into<Location>, _: Assets) -> xcm::latest::Result {
210			Ok(())
211		}
212	}
213
214	#[derive(Debug, Decode, Default)]
215	pub struct MockCall;
216	impl GetDispatchInfo for MockCall {
217		fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo {
218			Default::default()
219		}
220	}
221
222	pub struct MockXcmSender;
223	impl SendXcm for MockXcmSender {
224		type Ticket = Xcm<()>;
225
226		fn validate(
227			dest: &mut Option<Location>,
228			xcm: &mut Option<Xcm<()>>,
229		) -> SendResult<Self::Ticket> {
230			if let Some(location) = dest {
231				match location.unpack() {
232					(_, [Parachain(1001)]) => return Err(SendError::NotApplicable),
233					_ => Ok((xcm.clone().unwrap(), Assets::default())),
234				}
235			} else {
236				Ok((xcm.clone().unwrap(), Assets::default()))
237			}
238		}
239
240		fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
241			let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
242			Ok(hash)
243		}
244	}
245
246	#[test]
247	fn pay_reward_success() {
248		let relayer = MockRelayer(AccountId32::new([1u8; 32]));
249		let beneficiary = Location::new(1, Here);
250		let reward = 1_000u128;
251
252		type TestedPayAccountOnLocation = PayAccountOnLocation<
253			MockRelayer,
254			u128,
255			EthereumNetwork,
256			AssetHubLocation,
257			InboundQueueLocation,
258			MockXcmSender,
259			MockXcmExecutor,
260			MockCall,
261		>;
262
263		let result = TestedPayAccountOnLocation::pay_reward(&relayer, (), reward, beneficiary);
264
265		assert!(result.is_ok());
266	}
267
268	#[test]
269	fn pay_reward_fails_on_xcm_validate_xcm() {
270		struct FailingXcmValidator;
271		impl SendXcm for FailingXcmValidator {
272			type Ticket = ();
273
274			fn validate(
275				_dest: &mut Option<Location>,
276				_xcm: &mut Option<Xcm<()>>,
277			) -> SendResult<Self::Ticket> {
278				Err(SendError::NotApplicable)
279			}
280
281			fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
282				let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
283				Ok(hash)
284			}
285		}
286
287		type FailingSenderPayAccount = PayAccountOnLocation<
288			MockRelayer,
289			u128,
290			EthereumNetwork,
291			AssetHubLocation,
292			InboundQueueLocation,
293			FailingXcmValidator,
294			MockXcmExecutor,
295			MockCall,
296		>;
297
298		let relayer = MockRelayer(AccountId32::new([1u8; 32]));
299		let reward = 1_000u128;
300		let beneficiary = Location::new(1, Here);
301		let result = FailingSenderPayAccount::pay_reward(&relayer, (), reward, beneficiary);
302
303		assert!(result.is_err());
304		let err_str = format!("{:?}", result.err().unwrap());
305		assert!(
306			err_str.contains("xcm send failure"),
307			"Expected xcm send failure error, got {:?}",
308			err_str
309		);
310	}
311
312	#[test]
313	fn pay_reward_fails_on_charge_fees() {
314		struct FailingXcmExecutor;
315		impl<C> ExecuteXcm<C> for FailingXcmExecutor {
316			type Prepared = Weightless;
317			fn prepare(_: Xcm<C>, _: Weight) -> Result<Self::Prepared, InstructionError> {
318				Err(InstructionError { index: 0, error: XcmError::Unimplemented })
319			}
320			fn execute(
321				_: impl Into<Location>,
322				_: Self::Prepared,
323				_: &mut XcmHash,
324				_: Weight,
325			) -> Outcome {
326				unreachable!()
327			}
328			fn charge_fees(_: impl Into<Location>, _: Assets) -> xcm::latest::Result {
329				Err(crate::reward::SendError::Fees.into())
330			}
331		}
332
333		type FailingExecutorPayAccount = PayAccountOnLocation<
334			MockRelayer,
335			u128,
336			EthereumNetwork,
337			AssetHubLocation,
338			InboundQueueLocation,
339			MockXcmSender,
340			FailingXcmExecutor,
341			MockCall,
342		>;
343
344		let relayer = MockRelayer(AccountId32::new([3u8; 32]));
345		let beneficiary = Location::new(1, Here);
346		let reward = 500u128;
347		let result = FailingExecutorPayAccount::pay_reward(&relayer, (), reward, beneficiary);
348
349		assert!(result.is_err());
350		let err_str = format!("{:?}", result.err().unwrap());
351		assert!(
352			err_str.contains("charge fees error"),
353			"Expected 'charge fees error', got {:?}",
354			err_str
355		);
356	}
357
358	#[test]
359	fn pay_reward_fails_on_delivery() {
360		#[derive(Default)]
361		struct FailingDeliveryXcmSender;
362		impl SendXcm for FailingDeliveryXcmSender {
363			type Ticket = ();
364
365			fn validate(
366				_dest: &mut Option<Location>,
367				_xcm: &mut Option<Xcm<()>>,
368			) -> SendResult<Self::Ticket> {
369				Ok(((), Assets::from(vec![])))
370			}
371
372			fn deliver(_xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
373				Err(SendError::NotApplicable)
374			}
375		}
376
377		type FailingDeliveryPayAccount = PayAccountOnLocation<
378			MockRelayer,
379			u128,
380			EthereumNetwork,
381			AssetHubLocation,
382			InboundQueueLocation,
383			FailingDeliveryXcmSender,
384			MockXcmExecutor,
385			MockCall,
386		>;
387
388		let relayer = MockRelayer(AccountId32::new([4u8; 32]));
389		let beneficiary = Location::new(1, Here);
390		let reward = 123u128;
391		let result = FailingDeliveryPayAccount::pay_reward(&relayer, (), reward, beneficiary);
392
393		assert!(result.is_err());
394		let err_str = format!("{:?}", result.err().unwrap());
395		assert!(
396			err_str.contains("xcm send failure"),
397			"Expected 'xcm delivery failure', got {:?}",
398			err_str
399		);
400	}
401}