referrerpolicy=no-referrer-when-downgrade

snowbridge_outbound_queue_primitives/v2/converter/
convert.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Converts XCM messages into InboundMessage that can be processed by the Gateway contract
4
5use codec::DecodeAll;
6use core::slice::Iter;
7use frame_support::{ensure, BoundedVec};
8use snowbridge_core::{AgentIdOf, TokenId, TokenIdOf};
9
10use crate::v2::{
11	message::{Command, Message},
12	ContractCall,
13};
14
15use crate::v2::convert::XcmConverterError::{AssetResolutionFailed, FilterDoesNotConsumeAllAssets};
16use sp_core::H160;
17use sp_runtime::traits::MaybeConvert;
18use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
19use xcm::prelude::*;
20use xcm_executor::traits::ConvertLocation;
21use XcmConverterError::*;
22
23/// Errors that can be thrown to the pattern matching step.
24#[derive(PartialEq, Debug)]
25pub enum XcmConverterError {
26	UnexpectedEndOfXcm,
27	EndOfXcmMessageExpected,
28	WithdrawAssetExpected,
29	DepositAssetExpected,
30	NoReserveAssets,
31	FilterDoesNotConsumeAllAssets,
32	TooManyAssets,
33	ZeroAssetTransfer,
34	BeneficiaryResolutionFailed,
35	AssetResolutionFailed,
36	InvalidFeeAsset,
37	SetTopicExpected,
38	ReserveAssetDepositedExpected,
39	InvalidAsset,
40	UnexpectedInstruction,
41	TooManyCommands,
42	AliasOriginExpected,
43	InvalidOrigin,
44	TransactDecodeFailed,
45	TransactParamsDecodeFailed,
46	FeeAssetResolutionFailed,
47	CallContractValueInsufficient,
48	NoCommands,
49}
50
51macro_rules! match_expression {
52	($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
53		match $expression {
54			$( $pattern )|+ $( if $guard )? => Some($value),
55			_ => None,
56		}
57	};
58}
59
60pub struct XcmConverter<'a, ConvertAssetId, Call> {
61	iter: Peekable<Iter<'a, Instruction<Call>>>,
62	ethereum_network: NetworkId,
63	_marker: PhantomData<ConvertAssetId>,
64}
65impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
66where
67	ConvertAssetId: MaybeConvert<TokenId, Location>,
68{
69	pub fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId) -> Self {
70		Self {
71			iter: message.inner().iter().peekable(),
72			ethereum_network,
73			_marker: Default::default(),
74		}
75	}
76
77	fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
78		self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
79	}
80
81	fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
82		self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
83	}
84
85	fn network_matches(&self, network: &Option<NetworkId>) -> bool {
86		if let Some(network) = network {
87			*network == self.ethereum_network
88		} else {
89			true
90		}
91	}
92
93	/// Extract the fee asset item from PayFees(V5)
94	fn extract_remote_fee(&mut self) -> Result<u128, XcmConverterError> {
95		use XcmConverterError::*;
96		let reserved_fee_assets = match_expression!(self.next()?, WithdrawAsset(fee), fee)
97			.ok_or(WithdrawAssetExpected)?;
98		ensure!(reserved_fee_assets.len() == 1, AssetResolutionFailed);
99		let reserved_fee_asset =
100			reserved_fee_assets.inner().first().cloned().ok_or(AssetResolutionFailed)?;
101		let (reserved_fee_asset_id, reserved_fee_amount) = match reserved_fee_asset {
102			Asset { id: asset_id, fun: Fungible(amount) } => Ok((asset_id, amount)),
103			_ => Err(AssetResolutionFailed),
104		}?;
105		let fee_asset =
106			match_expression!(self.next()?, PayFees { asset: fee }, fee).ok_or(InvalidFeeAsset)?;
107		let (fee_asset_id, fee_amount) = match fee_asset {
108			Asset { id: asset_id, fun: Fungible(amount) } => Ok((asset_id, *amount)),
109			_ => Err(AssetResolutionFailed),
110		}?;
111		// Check the fee asset is Ether (XCM is evaluated in Ethereum context).
112		ensure!(fee_asset_id.0 == Here.into(), InvalidFeeAsset);
113		ensure!(reserved_fee_asset_id.0 == Here.into(), InvalidFeeAsset);
114		ensure!(reserved_fee_amount >= fee_amount, InvalidFeeAsset);
115		Ok(fee_amount)
116	}
117
118	/// Extract ethereum native assets
119	fn extract_ethereum_native_assets(
120		&mut self,
121		enas: &Assets,
122		deposit_assets: &AssetFilter,
123		recipient: H160,
124	) -> Result<Vec<Command>, XcmConverterError> {
125		let mut commands: Vec<Command> = Vec::new();
126		for ena in enas.clone().into_inner().into_iter() {
127			// Check the the deposit asset filter matches what was reserved.
128			if !deposit_assets.matches(&ena) {
129				return Err(FilterDoesNotConsumeAllAssets);
130			}
131
132			// only fungible asset is allowed
133			let (token, amount) = match ena {
134				Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
135					match inner_location.unpack() {
136						(0, [AccountKey20 { network, key }]) if self.network_matches(network) =>
137							Ok((H160(*key), amount)),
138						// To allow ether
139						(0, []) => Ok((H160([0; 20]), amount)),
140						_ => Err(AssetResolutionFailed),
141					},
142				_ => Err(AssetResolutionFailed),
143			}?;
144
145			// transfer amount must be greater than 0.
146			ensure!(amount > 0, ZeroAssetTransfer);
147
148			commands.push(Command::UnlockNativeToken { token, recipient, amount });
149		}
150		Ok(commands)
151	}
152
153	/// Extract polkadot native assets
154	fn extract_polkadot_native_assets(
155		&mut self,
156		pnas: &Assets,
157		deposit_assets: &AssetFilter,
158		recipient: H160,
159	) -> Result<Vec<Command>, XcmConverterError> {
160		let mut commands: Vec<Command> = Vec::new();
161		ensure!(pnas.len() > 0, NoReserveAssets);
162		for pna in pnas.clone().into_inner().into_iter() {
163			if !deposit_assets.matches(&pna) {
164				return Err(FilterDoesNotConsumeAllAssets);
165			}
166
167			// Only fungible is allowed
168			let Asset { id: AssetId(asset_id), fun: Fungible(amount) } = pna else {
169				return Err(AssetResolutionFailed);
170			};
171
172			// transfer amount must be greater than 0.
173			ensure!(amount > 0, ZeroAssetTransfer);
174
175			// Ensure PNA already registered
176			let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
177			ConvertAssetId::maybe_convert(token_id).ok_or(InvalidAsset)?;
178
179			commands.push(Command::MintForeignToken { token_id, recipient, amount });
180		}
181		Ok(commands)
182	}
183
184	/// Convert the XCM into an outbound message which can be dispatched to
185	/// the Gateway contract on Ethereum
186	///
187	/// Assets being transferred can either be Polkadot-native assets (PNA)
188	/// or Ethereum-native assets (ENA).
189	///
190	/// The XCM is evaluated in Ethereum context.
191	///
192	/// Expected Input Syntax:
193	/// ```ignore
194	/// WithdrawAsset(ETH)
195	/// PayFees(ETH)
196	/// ReserveAssetDeposited(PNA) | WithdrawAsset(ENA)
197	/// AliasOrigin(Origin)
198	/// DepositAsset(Asset)
199	/// Transact() [OPTIONAL]
200	/// SetTopic(Topic)
201	/// ```
202	/// Notes:
203	/// a. Fee asset will be checked and currently only Ether is allowed
204	/// b. For a specific transfer, either `ReserveAssetDeposited` or `WithdrawAsset` should be
205	/// 	present
206	/// c. `ReserveAssetDeposited` and `WithdrawAsset` can also be present in any order within the
207	/// 	same message
208	/// d. Currently, teleport asset is not allowed, transfer types other than
209	/// 	above will cause the conversion to fail
210	/// e. Currently, `AliasOrigin` is always required, can distinguish the V2 process from V1.
211	/// 	it's required also for dispatching transact from that specific origin.
212	/// f. SetTopic is required for tracing the message all the way along.
213	pub fn convert(&mut self) -> Result<Message, XcmConverterError> {
214		// Get fee amount
215		let fee_amount = self.extract_remote_fee()?;
216
217		// Get ENA reserve asset from WithdrawAsset.
218		let mut enas =
219			match_expression!(self.peek(), Ok(WithdrawAsset(reserve_assets)), reserve_assets);
220		if enas.is_some() {
221			let _ = self.next();
222		}
223
224		// Get PNA reserve asset from ReserveAssetDeposited
225		let pnas = match_expression!(
226			self.peek(),
227			Ok(ReserveAssetDeposited(reserve_assets)),
228			reserve_assets
229		);
230		if pnas.is_some() {
231			let _ = self.next();
232		}
233
234		// Try to get ENA again if it is after PNA
235		if enas.is_none() {
236			enas =
237				match_expression!(self.peek(), Ok(WithdrawAsset(reserve_assets)), reserve_assets);
238			if enas.is_some() {
239				let _ = self.next();
240			}
241		}
242		// Check AliasOrigin.
243		let origin_location = match_expression!(self.next()?, AliasOrigin(origin), origin)
244			.ok_or(AliasOriginExpected)?;
245		let origin = AgentIdOf::convert_location(origin_location).ok_or(InvalidOrigin)?;
246
247		let (deposit_assets, beneficiary) = match_expression!(
248			self.next()?,
249			DepositAsset { assets, beneficiary },
250			(assets, beneficiary)
251		)
252		.ok_or(DepositAssetExpected)?;
253
254		// assert that the beneficiary is AccountKey20.
255		let recipient = match_expression!(
256			beneficiary.unpack(),
257			(0, [AccountKey20 { network, key }])
258				if self.network_matches(network),
259			H160(*key)
260		)
261		.ok_or(BeneficiaryResolutionFailed)?;
262
263		let mut commands: Vec<Command> = Vec::new();
264
265		// ENA transfer commands
266		if let Some(enas) = enas {
267			commands.append(&mut self.extract_ethereum_native_assets(
268				enas,
269				deposit_assets,
270				recipient,
271			)?);
272		}
273
274		// PNA transfer commands
275		if let Some(pnas) = pnas {
276			commands.append(&mut self.extract_polkadot_native_assets(
277				pnas,
278				deposit_assets,
279				recipient,
280			)?);
281		}
282
283		// Transact commands
284		let transact_call = match_expression!(self.peek(), Ok(Transact { call, .. }), call);
285		if let Some(transact_call) = transact_call {
286			let _ = self.next();
287			let transact =
288				ContractCall::decode_all(&mut transact_call.clone().into_encoded().as_slice())
289					.map_err(|_| TransactDecodeFailed)?;
290			match transact {
291				ContractCall::V1 { target, calldata, gas, value } => commands
292					.push(Command::CallContract { target: target.into(), calldata, gas, value }),
293			}
294		}
295
296		ensure!(commands.len() > 0, NoCommands);
297
298		// ensure SetTopic exists
299		let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
300
301		let message = Message {
302			id: (*topic_id).into(),
303			origin,
304			fee: fee_amount,
305			commands: BoundedVec::try_from(commands).map_err(|_| TooManyCommands)?,
306		};
307
308		// All xcm instructions must be consumed before exit.
309		if self.next().is_ok() {
310			return Err(EndOfXcmMessageExpected);
311		}
312
313		Ok(message)
314	}
315}