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
mod raw_message_processor;
21
mod symbiotic_message_processor;
22

            
23
pub use raw_message_processor::RawMessageProcessor;
24
pub use symbiotic_message_processor::SymbioticMessageProcessor;
25

            
26
use alloc::vec;
27
use alloc::{boxed::Box, string::String, vec::Vec};
28

            
29
use thiserror::Error;
30

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

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

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

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

            
55
impl core::error::Error for CodecError {}
56

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

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

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

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

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

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

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

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

            
186
17
            let asset: Asset = (asset_location.clone(), *value).into();
187

            
188
            // If the asset_location has a Parachain as the first interior junction,
189
            // it means the asset is native to a parachain and was reserve-transferred
190
            // to Ethereum. We return ReserveDeposit in this case.
191
17
            let is_parachain_native = matches!(asset_location.interior.first(), Some(Parachain(_)));
192

            
193
17
            if is_parachain_native {
194
3
                Ok(AssetTransfer::ReserveDeposit(asset))
195
            } else {
196
14
                Ok(AssetTransfer::ReserveWithdraw(asset))
197
            }
198
        }
199
    }
200
25
}
201

            
202
390
pub fn derive_asset_for_native_eth(
203
390
    eth_chain_universal_location: &InteriorLocation,
204
390
    tanssi_chain_universal_location: &InteriorLocation,
205
390
    value: u128,
206
390
) -> Result<Asset, LocationConversionError> {
207
390
    let native_eth_reanchored_to_tanssi = reanchor_location_to_tanssi(
208
390
        eth_chain_universal_location,
209
390
        tanssi_chain_universal_location,
210
390
        ().into(),
211
    )?;
212
390
    Ok((native_eth_reanchored_to_tanssi, value).into())
213
390
}
214

            
215
25
pub fn derive_asset_transfers<T>(
216
25
    eth_network_id: NetworkId,
217
25
    eth_chain_universal_location: &InteriorLocation,
218
25
    tanssi_chain_universal_location: &InteriorLocation,
219
25
    assets: Vec<EthereumAsset>,
220
25
    eth_asset: u128,
221
25
) -> Result<Vec<AssetTransfer>, LocationConversionError>
222
25
where
223
25
    T: snowbridge_pallet_system::Config,
224
{
225
25
    let mut asset_transfers = vec![];
226
50
    for asset in assets {
227
25
        let asset_transfer = derive_asset_transfer_eth_asset::<T>(
228
25
            eth_network_id,
229
25
            eth_chain_universal_location,
230
25
            &asset,
231
25
            tanssi_chain_universal_location,
232
        )?;
233
25
        asset_transfers.push(asset_transfer);
234
    }
235

            
236
25
    if eth_asset > 0 {
237
12
        let native_eth_asset = derive_asset_for_native_eth(
238
12
            eth_chain_universal_location,
239
12
            tanssi_chain_universal_location,
240
12
            eth_asset,
241
        )?;
242
12
        asset_transfers.push(AssetTransfer::ReserveDeposit(native_eth_asset));
243
13
    }
244

            
245
25
    Ok(asset_transfers)
246
25
}
247

            
248
25
pub fn prepare_raw_message_xcm_instructions<T>(
249
25
    eth_network_id: NetworkId,
250
25
    eth_chain_universal_location: &InteriorLocation,
251
25
    tanssi_chain_universal_location: &InteriorLocation,
252
25
    gateway_proxy_address: H160,
253
25
    default_claimer: T::AccountId,
254
25
    topic_prefix: &str,
255
25
    extracted_xcm_construction_info: ExtractedXcmConstructionInfo<
256
25
        <T as pallet_xcm::Config>::RuntimeCall,
257
25
    >,
258
25
) -> Result<Vec<Instruction<<T as pallet_xcm::Config>::RuntimeCall>>, LocationConversionError>
259
25
where
260
25
    T: snowbridge_pallet_system::Config + pallet_xcm::Config,
261
25
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
262
{
263
    let ExtractedXcmConstructionInfo {
264
25
        origin,
265
25
        maybe_claimer,
266
25
        eth_value,
267
25
        assets,
268
25
        execution_fee_in_eth,
269
25
        nonce,
270
25
        user_xcm,
271
25
    } = extracted_xcm_construction_info;
272

            
273
25
    let claimer = maybe_claimer
274
        // Get the claimer from the message,
275
25
        .and_then(|claimer_bytes| Location::decode(&mut claimer_bytes.as_ref()).ok())
276
        // or use default claimer passed
277
25
        .unwrap_or_else(|| {
278
22
            Location::new(
279
                0,
280
22
                [AccountId32 {
281
22
                    network: None,
282
22
                    id: default_claimer.clone().into(),
283
22
                }],
284
            )
285
22
        });
286

            
287
    // derive asset transfers
288
25
    let asset_transfers = derive_asset_transfers::<T>(
289
25
        eth_network_id,
290
25
        eth_chain_universal_location,
291
25
        tanssi_chain_universal_location,
292
25
        assets,
293
25
        eth_value,
294
    )?;
295

            
296
25
    let mut instructions = vec![SetHints {
297
25
        hints: vec![AssetClaimer { location: claimer }]
298
25
            .try_into()
299
25
            .expect("checked statically, qed"),
300
25
    }];
301

            
302
25
    if execution_fee_in_eth > 0 {
303
3
        let execution_fee_asset = derive_asset_for_native_eth(
304
3
            eth_chain_universal_location,
305
3
            tanssi_chain_universal_location,
306
3
            execution_fee_in_eth,
307
        )?;
308
3
        instructions.push(ReserveAssetDeposited(execution_fee_asset.clone().into()));
309
22
    }
310

            
311
25
    let mut reserve_deposit_assets = vec![];
312
25
    let mut reserve_withdraw_assets = vec![];
313

            
314
62
    for asset in asset_transfers {
315
37
        match asset {
316
23
            AssetTransfer::ReserveDeposit(asset) => reserve_deposit_assets.push(asset),
317
14
            AssetTransfer::ReserveWithdraw(asset) => reserve_withdraw_assets.push(asset),
318
        };
319
    }
320

            
321
25
    if !reserve_deposit_assets.is_empty() {
322
20
        instructions.push(ReserveAssetDeposited(reserve_deposit_assets.into()));
323
20
    }
324
25
    if !reserve_withdraw_assets.is_empty() {
325
14
        instructions.push(WithdrawAsset(reserve_withdraw_assets.into()));
326
20
    }
327

            
328
    // Append DescendOrigin
329
25
    if origin != gateway_proxy_address {
330
5
        instructions.push(DescendOrigin(
331
5
            AccountKey20 {
332
5
                key: origin.into(),
333
5
                network: None,
334
5
            }
335
5
            .into(),
336
5
        ));
337
20
    }
338

            
339
    // Append raw xcm
340
25
    instructions.extend(user_xcm.0);
341

            
342
    // Add SetTopic instruction if not already present as the last instruction
343
25
    if !matches!(instructions.last(), Some(SetTopic(_))) {
344
25
        let topic = blake2_256(&(topic_prefix, nonce).encode());
345
25
        instructions.push(SetTopic(topic));
346
25
    }
347

            
348
25
    Ok(instructions)
349
25
}
350

            
351
21
pub fn execute_xcm<T, XcmProcessor, XcmWeigher>(
352
21
    origin: impl Into<Location>,
353
21
    max_xcm_weight: Weight,
354
21
    mut xcm: Xcm<<T as pallet_xcm::Config>::RuntimeCall>,
355
21
) -> Result<Outcome, InstructionError>
356
21
where
357
21
    T: pallet_xcm::Config,
358
21
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
359
21
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
360
{
361
21
    let weight = XcmWeigher::weight(&mut xcm, max_xcm_weight)?;
362

            
363
20
    let mut message_id = xcm.using_encoded(blake2_256);
364

            
365
20
    Ok(XcmProcessor::prepare_and_execute(
366
20
        origin,
367
20
        xcm,
368
20
        &mut message_id,
369
20
        weight,
370
20
        weight,
371
20
    ))
372
21
}
373

            
374
572
fn calculate_message_hash(message: &Message) -> [u8; 32] {
375
572
    blake2_256(message.encode().as_slice())
376
572
}
377

            
378
pub trait FallbackMessageProcessor<AccountId> {
379
    fn handle_message(
380
        who: AccountId,
381
        message: Message,
382
    ) -> Result<Option<Weight>, MessageProcessorError>;
383
}
384

            
385
pub trait MessageProcessorWithFallback<AccountId> {
386
    type Fallback: FallbackMessageProcessor<AccountId>;
387
    type ExtractedMessage;
388

            
389
    fn try_extract_message(
390
        sender: &AccountId,
391
        message: &Message,
392
    ) -> Result<Self::ExtractedMessage, MessageExtractionError>;
393

            
394
    fn process_extracted_message(
395
        sender: AccountId,
396
        extracted_message: Self::ExtractedMessage,
397
    ) -> Result<Option<Weight>, MessageProcessorError>;
398

            
399
    fn worst_case_message_processor_weight() -> Weight;
400

            
401
22
    fn calculate_message_id(message: &Message) -> [u8; 32] {
402
22
        calculate_message_hash(message)
403
22
    }
404
}
405

            
406
#[cfg(test)]
407
mod tests {
408
    use crate::processors::v2::reanchor_location_to_tanssi;
409
    use xcm::latest::prelude::*;
410

            
411
    #[test]
412
1
    fn reanchor_works_for_tanssi_interior() {
413
1
        tanssi_interior_reanchor_test(true);
414
1
        tanssi_interior_reanchor_test(false);
415
1
    }
416

            
417
2
    fn tanssi_interior_reanchor_test(should_reanchor_tanssi_location: bool) {
418
2
        let context: InteriorLocation = GlobalConsensus(Ethereum { chain_id: 4 }).into();
419
2
        let mut tanssi_location: Location = GlobalConsensus(ByGenesis([2; 32])).into();
420
2
        let mut tanssi_interior_anchored_to_eth: Location = (
421
2
            Parent,
422
2
            GlobalConsensus(ByGenesis([2; 32])),
423
2
            AccountId32 {
424
2
                network: None,
425
2
                id: [1; 32],
426
2
            },
427
2
        )
428
2
            .into();
429
2
        let expected = AccountId32 {
430
2
            network: None,
431
2
            id: [1; 32],
432
2
        }
433
2
        .into();
434
2
        let generated_by_func = reanchor_location_to_tanssi(
435
2
            &context,
436
2
            &tanssi_location.interior,
437
2
            tanssi_interior_anchored_to_eth.clone(),
438
        )
439
2
        .unwrap();
440
2
        assert_eq!(generated_by_func, expected);
441

            
442
2
        if should_reanchor_tanssi_location {
443
1
            tanssi_location
444
1
                .reanchor(&context.clone().into(), &().into())
445
1
                .unwrap();
446
1
        }
447

            
448
2
        let target = tanssi_location;
449
2
        tanssi_interior_anchored_to_eth
450
2
            .reanchor(&target, &context)
451
2
            .unwrap();
452
2
        if should_reanchor_tanssi_location {
453
1
            assert_eq!(tanssi_interior_anchored_to_eth, expected);
454
        } else {
455
1
            assert_ne!(tanssi_interior_anchored_to_eth, expected);
456
        }
457
2
    }
458

            
459
    #[test]
460
1
    fn reanchor_works_for_eth_interior() {
461
1
        eth_interior_reanchor_test(true);
462
1
        eth_interior_reanchor_test(false);
463
1
    }
464

            
465
2
    fn eth_interior_reanchor_test(should_reanchor_tanssi_location: bool) {
466
2
        let context: InteriorLocation = GlobalConsensus(Ethereum { chain_id: 4 }).into();
467
2
        let mut tanssi_location: Location = GlobalConsensus(ByGenesis([2; 32])).into();
468
2
        let mut eth_interior: Location = (AccountKey20 {
469
2
            network: Some(Ethereum { chain_id: 4 }),
470
2
            key: [5; 20],
471
2
        })
472
2
        .into();
473
2
        let expected = (
474
2
            Parent,
475
2
            GlobalConsensus(Ethereum { chain_id: 4 }),
476
2
            AccountKey20 {
477
2
                network: Some(Ethereum { chain_id: 4 }),
478
2
                key: [5; 20],
479
2
            },
480
2
        )
481
2
            .into();
482

            
483
2
        let generated_by_func =
484
2
            reanchor_location_to_tanssi(&context, &tanssi_location.interior, eth_interior.clone())
485
2
                .unwrap();
486
2
        assert_eq!(generated_by_func, expected);
487

            
488
2
        if should_reanchor_tanssi_location {
489
1
            tanssi_location
490
1
                .reanchor(&context.clone().into(), &().into())
491
1
                .unwrap();
492
1
        }
493

            
494
2
        let target = tanssi_location;
495
2
        eth_interior.reanchor(&target, &context).unwrap();
496

            
497
2
        if should_reanchor_tanssi_location {
498
1
            assert_eq!(eth_interior, expected);
499
        } else {
500
1
            assert_ne!(eth_interior, expected);
501
        }
502
2
    }
503
}