1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
extern crate alloc;
18

            
19
mod fallback_message_processor;
20
pub mod layerzero_message_processor;
21
mod raw_message_processor;
22
mod symbiotic_message_processor;
23

            
24
pub use layerzero_message_processor::LayerZeroMessageProcessor;
25
pub use raw_message_processor::RawMessageProcessor;
26
pub use symbiotic_message_processor::SymbioticMessageProcessor;
27

            
28
use alloc::vec;
29
use alloc::{boxed::Box, string::String, vec::Vec};
30

            
31
use thiserror::Error;
32

            
33
use parity_scale_codec::{Decode, Encode};
34
use snowbridge_inbound_queue_primitives::v2::{
35
    AssetTransfer, EthereumAsset, Message, MessageProcessorError,
36
};
37
use sp_core::{H160, H256};
38
use sp_io::hashing::blake2_256;
39
use sp_runtime::traits::MaybeEquivalence;
40
use sp_runtime::DispatchError;
41
use xcm::latest::prelude::*;
42
use xcm_executor::traits::WeightBounds;
43

            
44
/// Topic prefix used for generating unique identifiers for messages
45
pub const RAW_MESSAGE_PROCESSOR_TOPIC_PREFIX: &str = "TanssiRawMessageProcessor";
46

            
47
/// Wrapping parity_scale_codec::Error so that it implements Error
48
#[derive(Debug)]
49
pub struct CodecError(parity_scale_codec::Error);
50

            
51
impl core::fmt::Display for CodecError {
52
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
53
        write!(f, "{:?}", self.0)
54
    }
55
}
56

            
57
impl core::error::Error for CodecError {}
58

            
59
impl From<parity_scale_codec::Error> for CodecError {
60
22
    fn from(e: parity_scale_codec::Error) -> Self {
61
22
        CodecError(e)
62
22
    }
63
}
64

            
65
#[derive(Error, Debug)]
66
pub enum MessageExtractionError {
67
    #[error("Unsupported Message: {context} due to {source:?}")]
68
    UnsupportedMessage {
69
        context: String,
70
        source: Option<Box<dyn core::error::Error + Send + Sync>>,
71
    },
72
    #[error("Invalid Message: {context} due to {source:?}")]
73
    InvalidMessage {
74
        context: String,
75
        source: Option<Box<dyn core::error::Error + Send + Sync>>,
76
    },
77
    #[error("Other error: {context} due to {source:?}")]
78
    Other {
79
        context: String,
80
        source: Option<Box<dyn core::error::Error + Send + Sync>>,
81
    },
82
}
83

            
84
#[derive(Error, Debug)]
85
pub enum LocationConversionError {
86
    #[error("Unable to reanchor {location:?}")]
87
    UnableToReanchor { location: Location },
88
    #[error("Unable to convert {token_id} in location")]
89
    UnableToConvertTokenId { token_id: H256 },
90
}
91

            
92
impl Into<MessageProcessorError> for MessageExtractionError {
93
806
    fn into(self) -> MessageProcessorError {
94
806
        match self {
95
            MessageExtractionError::UnsupportedMessage { .. } => {
96
338
                MessageProcessorError::ProcessMessage(DispatchError::Other(
97
338
                    "Unsupported v2 message",
98
338
                ))
99
            }
100
            MessageExtractionError::InvalidMessage { .. } => {
101
                MessageProcessorError::ProcessMessage(DispatchError::Other("Invalid v2 message"))
102
            }
103
468
            MessageExtractionError::Other { .. } => MessageProcessorError::ProcessMessage(
104
468
                DispatchError::Other("Other error while processing v2 message"),
105
468
            ),
106
        }
107
806
    }
108
}
109

            
110
#[derive(Encode, Decode, Clone, Debug)]
111
pub enum RawPayload {
112
    Xcm(Vec<u8>),
113
    Symbiotic(Vec<u8>),
114
    LayerZero(Vec<u8>),
115
}
116

            
117
#[derive(Debug, Clone)]
118
pub struct ExtractedXcmConstructionInfo<Call> {
119
    pub origin: H160,
120
    pub maybe_claimer: Option<Vec<u8>>,
121
    pub eth_value: u128,
122
    pub assets: Vec<EthereumAsset>,
123
    pub execution_fee_in_eth: u128,
124
    pub nonce: u64,
125
    pub user_xcm: Xcm<Call>,
126
}
127

            
128
862
fn reanchor_location_to_tanssi(
129
862
    eth_chain_universal_location: &InteriorLocation,
130
862
    tanssi_chain_universal_location: &InteriorLocation,
131
862
    location_anchored_to_eth: Location,
132
862
) -> Result<Location, LocationConversionError> {
133
862
    let tanssi_reanchored_to_eth = tanssi_chain_universal_location
134
862
        .clone()
135
862
        .into_location()
136
862
        .reanchored(&eth_chain_universal_location.clone().into(), &().into())
137
862
        .map_err(
138
            |original_location| LocationConversionError::UnableToReanchor {
139
                location: original_location,
140
            },
141
        )?;
142
862
    location_anchored_to_eth
143
862
        .reanchored(&tanssi_reanchored_to_eth, eth_chain_universal_location)
144
862
        .map_err(
145
            |original_location| LocationConversionError::UnableToReanchor {
146
                location: original_location,
147
            },
148
        )
149
862
}
150

            
151
11
pub fn derive_asset_transfer_eth_asset<T>(
152
11
    eth_network_id: NetworkId,
153
11
    eth_chain_universal_location: &InteriorLocation,
154
11
    asset: &EthereumAsset,
155
11
    tanssi_chain_universal_location: &InteriorLocation,
156
11
) -> Result<AssetTransfer, LocationConversionError>
157
11
where
158
11
    T: snowbridge_pallet_system::Config,
159
{
160
11
    match asset {
161
        // Native to eth
162
6
        EthereumAsset::NativeTokenERC20 { token_id, value } => {
163
6
            let asset_location = reanchor_location_to_tanssi(
164
6
                eth_chain_universal_location,
165
6
                tanssi_chain_universal_location,
166
6
                (AccountKey20 {
167
6
                    network: Some(eth_network_id),
168
6
                    key: token_id.0,
169
6
                })
170
6
                .into(),
171
            )?;
172
6
            let asset: Asset = (asset_location, *value).into();
173
6
            Ok(AssetTransfer::ReserveDeposit(asset))
174
        }
175
        // Foreign to eth
176
5
        EthereumAsset::ForeignTokenERC20 { token_id, value } => {
177
5
            let token_location_reanchored_to_eth = snowbridge_pallet_system::Pallet::<T>::convert(
178
5
                &token_id,
179
            )
180
5
            .ok_or(LocationConversionError::UnableToConvertTokenId {
181
5
                token_id: *token_id,
182
5
            })?;
183
5
            let asset_location = reanchor_location_to_tanssi(
184
5
                eth_chain_universal_location,
185
5
                tanssi_chain_universal_location,
186
5
                token_location_reanchored_to_eth,
187
            )?;
188
5
            let asset: Asset = (asset_location, *value).into();
189
5
            Ok(AssetTransfer::ReserveWithdraw(asset))
190
        }
191
    }
192
11
}
193

            
194
338
pub fn derive_asset_for_native_eth(
195
338
    eth_chain_universal_location: &InteriorLocation,
196
338
    tanssi_chain_universal_location: &InteriorLocation,
197
338
    value: u128,
198
338
) -> Result<Asset, LocationConversionError> {
199
338
    let native_eth_reanchored_to_tanssi = reanchor_location_to_tanssi(
200
338
        eth_chain_universal_location,
201
338
        tanssi_chain_universal_location,
202
338
        ().into(),
203
    )?;
204
338
    Ok((native_eth_reanchored_to_tanssi, value).into())
205
338
}
206

            
207
16
pub fn derive_asset_transfers<T>(
208
16
    eth_network_id: NetworkId,
209
16
    eth_chain_universal_location: &InteriorLocation,
210
16
    tanssi_chain_universal_location: &InteriorLocation,
211
16
    assets: Vec<EthereumAsset>,
212
16
    eth_asset: u128,
213
16
) -> Result<Vec<AssetTransfer>, LocationConversionError>
214
16
where
215
16
    T: snowbridge_pallet_system::Config,
216
{
217
16
    let mut asset_transfers = vec![];
218
27
    for asset in assets {
219
11
        let asset_transfer = derive_asset_transfer_eth_asset::<T>(
220
11
            eth_network_id,
221
11
            eth_chain_universal_location,
222
11
            &asset,
223
11
            tanssi_chain_universal_location,
224
        )?;
225
11
        asset_transfers.push(asset_transfer);
226
    }
227

            
228
16
    if eth_asset > 0 {
229
10
        let native_eth_asset = derive_asset_for_native_eth(
230
10
            eth_chain_universal_location,
231
10
            tanssi_chain_universal_location,
232
10
            eth_asset,
233
        )?;
234
10
        asset_transfers.push(AssetTransfer::ReserveDeposit(native_eth_asset));
235
6
    }
236

            
237
16
    Ok(asset_transfers)
238
16
}
239

            
240
16
pub fn prepare_raw_message_xcm_instructions<T>(
241
16
    eth_network_id: NetworkId,
242
16
    eth_chain_universal_location: &InteriorLocation,
243
16
    tanssi_chain_universal_location: &InteriorLocation,
244
16
    gateway_proxy_address: H160,
245
16
    default_claimer: T::AccountId,
246
16
    topic_prefix: &str,
247
16
    extracted_xcm_construction_info: ExtractedXcmConstructionInfo<
248
16
        <T as pallet_xcm::Config>::RuntimeCall,
249
16
    >,
250
16
) -> Result<Vec<Instruction<<T as pallet_xcm::Config>::RuntimeCall>>, LocationConversionError>
251
16
where
252
16
    T: snowbridge_pallet_system::Config + pallet_xcm::Config,
253
16
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
254
{
255
    let ExtractedXcmConstructionInfo {
256
16
        origin,
257
16
        maybe_claimer,
258
16
        eth_value,
259
16
        assets,
260
16
        execution_fee_in_eth,
261
16
        nonce,
262
16
        user_xcm,
263
16
    } = extracted_xcm_construction_info;
264

            
265
16
    let claimer = maybe_claimer
266
        // Get the claimer from the message,
267
16
        .and_then(|claimer_bytes| Location::decode(&mut claimer_bytes.as_ref()).ok())
268
        // or use default claimer passed
269
16
        .unwrap_or_else(|| {
270
13
            Location::new(
271
                0,
272
13
                [AccountId32 {
273
13
                    network: None,
274
13
                    id: default_claimer.clone().into(),
275
13
                }],
276
            )
277
13
        });
278

            
279
    // derive asset transfers
280
16
    let asset_transfers = derive_asset_transfers::<T>(
281
16
        eth_network_id,
282
16
        eth_chain_universal_location,
283
16
        tanssi_chain_universal_location,
284
16
        assets,
285
16
        eth_value,
286
    )?;
287

            
288
16
    let mut instructions = vec![SetHints {
289
16
        hints: vec![AssetClaimer { location: claimer }]
290
16
            .try_into()
291
16
            .expect("checked statically, qed"),
292
16
    }];
293

            
294
16
    if execution_fee_in_eth > 0 {
295
3
        let execution_fee_asset = derive_asset_for_native_eth(
296
3
            eth_chain_universal_location,
297
3
            tanssi_chain_universal_location,
298
3
            execution_fee_in_eth,
299
        )?;
300
3
        instructions.push(ReserveAssetDeposited(execution_fee_asset.clone().into()));
301
13
    }
302

            
303
16
    let mut reserve_deposit_assets = vec![];
304
16
    let mut reserve_withdraw_assets = vec![];
305

            
306
37
    for asset in asset_transfers {
307
21
        match asset {
308
16
            AssetTransfer::ReserveDeposit(asset) => reserve_deposit_assets.push(asset),
309
5
            AssetTransfer::ReserveWithdraw(asset) => reserve_withdraw_assets.push(asset),
310
        };
311
    }
312

            
313
16
    if !reserve_deposit_assets.is_empty() {
314
13
        instructions.push(ReserveAssetDeposited(reserve_deposit_assets.into()));
315
13
    }
316
16
    if !reserve_withdraw_assets.is_empty() {
317
5
        instructions.push(WithdrawAsset(reserve_withdraw_assets.into()));
318
11
    }
319

            
320
    // Append DescendOrigin
321
16
    if origin != gateway_proxy_address {
322
4
        instructions.push(DescendOrigin(
323
4
            AccountKey20 {
324
4
                key: origin.into(),
325
4
                network: None,
326
4
            }
327
4
            .into(),
328
4
        ));
329
12
    }
330

            
331
    // Append raw xcm
332
16
    instructions.extend(user_xcm.0);
333

            
334
    // Add SetTopic instruction if not already present as the last instruction
335
16
    if !matches!(instructions.last(), Some(SetTopic(_))) {
336
16
        let topic = blake2_256(&(topic_prefix, nonce).encode());
337
16
        instructions.push(SetTopic(topic));
338
16
    }
339

            
340
16
    Ok(instructions)
341
16
}
342

            
343
9
pub fn execute_xcm<T, XcmProcessor, XcmWeigher>(
344
9
    origin: impl Into<Location>,
345
9
    mut xcm: Xcm<<T as pallet_xcm::Config>::RuntimeCall>,
346
9
) -> Result<(), InstructionError>
347
9
where
348
9
    T: pallet_xcm::Config,
349
9
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
350
9
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
351
{
352
    // Using Weight::MAX here because we don't have a limit, same as they do in pallet-xcm
353
9
    let weight = XcmWeigher::weight(&mut xcm, Weight::MAX)?;
354

            
355
9
    let mut message_id = xcm.using_encoded(blake2_256);
356

            
357
9
    XcmProcessor::prepare_and_execute(origin, xcm, &mut message_id, weight, weight)
358
9
        .ensure_complete()
359
9
}
360

            
361
338
fn calculate_message_hash(message: &Message) -> [u8; 32] {
362
338
    blake2_256(message.encode().as_slice())
363
338
}
364

            
365
pub trait FallbackMessageProcessor<AccountId> {
366
    fn handle_message(
367
        who: AccountId,
368
        message: Message,
369
    ) -> Result<Option<Weight>, MessageProcessorError>;
370
}
371

            
372
pub trait MessageProcessorWithFallback<AccountId> {
373
    type Fallback: FallbackMessageProcessor<AccountId>;
374
    type ExtractedMessage;
375

            
376
    fn try_extract_message(
377
        sender: &AccountId,
378
        message: &Message,
379
    ) -> Result<Self::ExtractedMessage, MessageExtractionError>;
380

            
381
    fn process_extracted_message(
382
        sender: AccountId,
383
        extracted_message: Self::ExtractedMessage,
384
    ) -> Result<Option<Weight>, MessageProcessorError>;
385

            
386
13
    fn calculate_message_id(message: &Message) -> [u8; 32] {
387
13
        calculate_message_hash(message)
388
13
    }
389
}
390

            
391
#[cfg(test)]
392
mod tests {
393
    use crate::processors::v2::reanchor_location_to_tanssi;
394
    use xcm::latest::prelude::*;
395

            
396
    #[test]
397
1
    fn reanchor_works_for_tanssi_interior() {
398
1
        tanssi_interior_reanchor_test(true);
399
1
        tanssi_interior_reanchor_test(false);
400
1
    }
401

            
402
2
    fn tanssi_interior_reanchor_test(should_reanchor_tanssi_location: bool) {
403
2
        let context: InteriorLocation = GlobalConsensus(Ethereum { chain_id: 4 }).into();
404
2
        let mut tanssi_location: Location = GlobalConsensus(ByGenesis([2; 32])).into();
405
2
        let mut tanssi_interior_anchored_to_eth: Location = (
406
2
            Parent,
407
2
            GlobalConsensus(ByGenesis([2; 32])),
408
2
            AccountId32 {
409
2
                network: None,
410
2
                id: [1; 32],
411
2
            },
412
2
        )
413
2
            .into();
414
2
        let expected = AccountId32 {
415
2
            network: None,
416
2
            id: [1; 32],
417
2
        }
418
2
        .into();
419
2
        let generated_by_func = reanchor_location_to_tanssi(
420
2
            &context,
421
2
            &tanssi_location.interior,
422
2
            tanssi_interior_anchored_to_eth.clone(),
423
        )
424
2
        .unwrap();
425
2
        assert_eq!(generated_by_func, expected);
426

            
427
2
        if should_reanchor_tanssi_location {
428
1
            tanssi_location
429
1
                .reanchor(&context.clone().into(), &().into())
430
1
                .unwrap();
431
1
        }
432

            
433
2
        let target = tanssi_location;
434
2
        tanssi_interior_anchored_to_eth
435
2
            .reanchor(&target, &context)
436
2
            .unwrap();
437
2
        if should_reanchor_tanssi_location {
438
1
            assert_eq!(tanssi_interior_anchored_to_eth, expected);
439
        } else {
440
1
            assert_ne!(tanssi_interior_anchored_to_eth, expected);
441
        }
442
2
    }
443

            
444
    #[test]
445
1
    fn reanchor_works_for_eth_interior() {
446
1
        eth_interior_reanchor_test(true);
447
1
        eth_interior_reanchor_test(false);
448
1
    }
449

            
450
2
    fn eth_interior_reanchor_test(should_reanchor_tanssi_location: bool) {
451
2
        let context: InteriorLocation = GlobalConsensus(Ethereum { chain_id: 4 }).into();
452
2
        let mut tanssi_location: Location = GlobalConsensus(ByGenesis([2; 32])).into();
453
2
        let mut eth_interior: Location = (AccountKey20 {
454
2
            network: Some(Ethereum { chain_id: 4 }),
455
2
            key: [5; 20],
456
2
        })
457
2
        .into();
458
2
        let expected = (
459
2
            Parent,
460
2
            GlobalConsensus(Ethereum { chain_id: 4 }),
461
2
            AccountKey20 {
462
2
                network: Some(Ethereum { chain_id: 4 }),
463
2
                key: [5; 20],
464
2
            },
465
2
        )
466
2
            .into();
467

            
468
2
        let generated_by_func =
469
2
            reanchor_location_to_tanssi(&context, &tanssi_location.interior, eth_interior.clone())
470
2
                .unwrap();
471
2
        assert_eq!(generated_by_func, expected);
472

            
473
2
        if should_reanchor_tanssi_location {
474
1
            tanssi_location
475
1
                .reanchor(&context.clone().into(), &().into())
476
1
                .unwrap();
477
1
        }
478

            
479
2
        let target = tanssi_location;
480
2
        eth_interior.reanchor(&target, &context).unwrap();
481

            
482
2
        if should_reanchor_tanssi_location {
483
1
            assert_eq!(eth_interior, expected);
484
        } else {
485
1
            assert_ne!(eth_interior, expected);
486
        }
487
2
    }
488
}