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
use crate::processors::v2::{
20
    execute_xcm, fallback_message_processor::AssetTrapFallbackProcessor,
21
    prepare_raw_message_xcm_instructions, CodecError, ExtractedXcmConstructionInfo,
22
    FallbackMessageProcessor, MessageExtractionError, MessageProcessorWithFallback, RawPayload,
23
};
24
use crate::processors::v2::{reanchor_location_to_tanssi, RAW_MESSAGE_PROCESSOR_TOPIC_PREFIX};
25
use alloc::boxed::Box;
26
use alloc::string::ToString;
27
use core::marker::PhantomData;
28
use parity_scale_codec::{Decode, DecodeLimit};
29
use snowbridge_inbound_queue_primitives::v2::{message::Message, MessageProcessorError, Payload};
30
use sp_core::{Get, H160};
31
use sp_runtime::DispatchError;
32
use sp_runtime::Weight;
33
use thiserror::Error;
34
use v2_processor_proc_macro::MessageProcessor;
35
use xcm::latest::{ExecuteXcm, Outcome};
36
use xcm::prelude::{InteriorLocation, NetworkId, VersionedXcm, Xcm};
37
use xcm::{IdentifyVersion, Version, MAX_XCM_DECODE_DEPTH};
38
use xcm_executor::traits::WeightBounds;
39

            
40
#[derive(Error, Debug)]
41
pub enum XcmDecodeError {
42
    #[error("Failed to decode versioned xcm message: {0}")]
43
    VersionedXcmDecodeError(#[from] CodecError),
44
    #[error("Xcm version {version} is not supported")]
45
    UnsupportedXcmVersion { version: Version },
46
}
47

            
48
/// Parse and strictly decode `raw` XCM bytes into a `Xcm<()>`.
49
35
fn decode_raw_xcm<T>(
50
35
    mut data: &[u8],
51
35
) -> Result<Xcm<<T as pallet_xcm::Config>::RuntimeCall>, XcmDecodeError>
52
35
where
53
35
    T: pallet_xcm::Config,
54
{
55
35
    VersionedXcm::<<T as pallet_xcm::Config>::RuntimeCall>::decode_with_depth_limit(
56
        MAX_XCM_DECODE_DEPTH,
57
35
        &mut data,
58
    )
59
35
    .map_err(|e| XcmDecodeError::VersionedXcmDecodeError(e.into()))
60
35
    .and_then(|xcm| {
61
33
        let version = xcm.identify_version();
62
33
        xcm.try_into()
63
33
            .map_err(|_| XcmDecodeError::UnsupportedXcmVersion { version })
64
33
    })
65
35
}
66

            
67
41
pub fn try_extract_message<T>(
68
41
    message: &Message,
69
41
) -> Result<
70
41
    ExtractedXcmConstructionInfo<<T as pallet_xcm::Config>::RuntimeCall>,
71
41
    MessageExtractionError,
72
41
>
73
41
where
74
41
    T: pallet_xcm::Config,
75
{
76
41
    match message.payload {
77
41
        Payload::Raw(ref payload) => {
78
41
            let raw_payload =
79
41
                RawPayload::decode(&mut payload.as_slice()).map_err(|decode_error| {
80
                    MessageExtractionError::InvalidMessage {
81
                        context: "Unable to decode RawPayload".to_string(),
82
                        source: Some(Box::new(CodecError(decode_error))),
83
                    }
84
                })?;
85
41
            match raw_payload {
86
35
                RawPayload::Xcm(payload) => Ok(decode_raw_xcm::<T>(&payload)
87
35
                    .map(|xcm| ExtractedXcmConstructionInfo {
88
33
                        origin: message.origin,
89
33
                        maybe_claimer: message.claimer.clone(),
90
33
                        assets: message.assets.clone(),
91
33
                        eth_value: message.value,
92
33
                        execution_fee_in_eth: message.execution_fee,
93
33
                        nonce: message.nonce,
94
33
                        user_xcm: xcm,
95
33
                    })
96
35
                    .map_err(|error| MessageExtractionError::InvalidMessage {
97
2
                        context: "Unable to decode Xcm".to_string(),
98
2
                        source: Some(Box::new(error)),
99
2
                    })?),
100
6
                RawPayload::Symbiotic(_) => Err(MessageExtractionError::UnsupportedMessage {
101
6
                    context: "Message is unsupported".to_string(),
102
6
                    source: None,
103
6
                }),
104
            }
105
        }
106
        _ => Err(MessageExtractionError::UnsupportedMessage {
107
            context: "Message is unsupported".to_string(),
108
            source: None,
109
        }),
110
    }
111
41
}
112

            
113
/// Processes raw XCM messages from Snowbridge V2 with automatic asset trapping fallback.
114
///
115
/// This processor handles messages containing user-provided XCM instructions encoded in the
116
/// `Payload::Raw(RawPayload::Xcm)` format. The workflow is:
117
///
118
/// 1. **Extract**: Decode the `VersionedXcm` from the message payload
119
/// 2. **Prepare**: Wrap user XCM
120
/// 3. **Execute**: Run the prepared XCM on Tanssi relay chain
121
///
122
/// # Fallback Behavior
123
///
124
/// Uses `AssetTrapFallbackProcessor` as fallback for unsupported message types (e.g., Symbiotic).
125
/// This ensures assets are never lost:
126
/// - If message extraction fails → fallback traps assets for claimer recovery
127
/// - If XCM execution fails → still returns success to prevent Ethereum revert, assets become
128
///   trapped and claimable
129
///
130
/// # Error Handling
131
///
132
/// Always returns success even on XCM execution errors to avoid leaving assets in limbo on
133
/// Ethereum. Execution errors are logged but don't cause transaction reversion. This is
134
/// necessary because user XCM may contain non-revertible operations (e.g., cross-chain messages).
135
#[derive(MessageProcessor)]
136
pub struct RawMessageProcessor<
137
    T,
138
    GatewayAddress,
139
    DefaultClaimer,
140
    EthereumNetwork,
141
    EthereumUniversalLocation,
142
    TanssiUniversalLocation,
143
    XcmProcessor,
144
    XcmWeigher,
145
    MaxXcmWeight,
146
>(
147
    PhantomData<(
148
        T,
149
        GatewayAddress,
150
        DefaultClaimer,
151
        EthereumNetwork,
152
        EthereumUniversalLocation,
153
        TanssiUniversalLocation,
154
        XcmProcessor,
155
        XcmWeigher,
156
        MaxXcmWeight,
157
    )>,
158
);
159

            
160
impl<
161
        T,
162
        AccountId,
163
        GatewayAddress,
164
        DefaultClaimer,
165
        EthereumNetwork,
166
        EthereumUniversalLocation,
167
        TanssiUniversalLocation,
168
        XcmProcessor,
169
        XcmWeigher,
170
        MaxXcmWeight,
171
    > MessageProcessorWithFallback<AccountId>
172
    for RawMessageProcessor<
173
        T,
174
        GatewayAddress,
175
        DefaultClaimer,
176
        EthereumNetwork,
177
        EthereumUniversalLocation,
178
        TanssiUniversalLocation,
179
        XcmProcessor,
180
        XcmWeigher,
181
        MaxXcmWeight,
182
    >
183
where
184
    T: snowbridge_pallet_inbound_queue::Config
185
        + pallet_xcm::Config
186
        + snowbridge_pallet_system::Config,
187
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
188
    GatewayAddress: Get<H160>,
189
    DefaultClaimer: Get<<T as frame_system::Config>::AccountId>,
190
    EthereumNetwork: Get<NetworkId>,
191
    EthereumUniversalLocation: Get<InteriorLocation>,
192
    TanssiUniversalLocation: Get<InteriorLocation>,
193
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
194
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
195
    MaxXcmWeight: Get<Weight>,
196
{
197
    type Fallback = AssetTrapFallbackProcessor<
198
        T,
199
        GatewayAddress,
200
        DefaultClaimer,
201
        EthereumNetwork,
202
        EthereumUniversalLocation,
203
        TanssiUniversalLocation,
204
        XcmProcessor,
205
        XcmWeigher,
206
        MaxXcmWeight,
207
    >;
208
    type ExtractedMessage = ExtractedXcmConstructionInfo<<T as pallet_xcm::Config>::RuntimeCall>;
209

            
210
41
    fn try_extract_message(
211
41
        _sender: &AccountId,
212
41
        message: &Message,
213
41
    ) -> Result<Self::ExtractedMessage, MessageExtractionError> {
214
41
        try_extract_message::<T>(message)
215
41
    }
216

            
217
16
    fn process_extracted_message(
218
16
        _sender: AccountId,
219
16
        extracted_message: Self::ExtractedMessage,
220
16
    ) -> Result<Option<Weight>, MessageProcessorError> {
221
16
        let prepared_xcm = prepare_raw_message_xcm_instructions::<T>(
222
16
            EthereumNetwork::get(),
223
16
            &EthereumUniversalLocation::get(),
224
16
            &TanssiUniversalLocation::get(),
225
16
            GatewayAddress::get(),
226
16
            DefaultClaimer::get(),
227
16
            RAW_MESSAGE_PROCESSOR_TOPIC_PREFIX,
228
16
            extracted_message,
229
        )
230
16
        .map_err(|location_conversion_error| {
231
            log::error!(
232
                "Error while preparing xcm instructions: {:?}",
233
                location_conversion_error
234
            );
235
            MessageProcessorError::ProcessMessage(DispatchError::Other(
236
                "Error while preparing xcm instructions",
237
            ))
238
        })?
239
16
        .into();
240

            
241
16
        let eth_location_reanchored_to_tanssi = reanchor_location_to_tanssi(
242
16
            &EthereumUniversalLocation::get(),
243
16
            &TanssiUniversalLocation::get(),
244
16
            ().into(),
245
        )
246
16
        .map_err(|location_conversion_error| {
247
            log::error!(
248
                "Unable to reanchor eth location to tanssi: {:?}",
249
                location_conversion_error
250
            );
251
            MessageProcessorError::ProcessMessage(DispatchError::Other(
252
                "Unable to reanchor eth location to tanssi",
253
            ))
254
        })?;
255

            
256
        // Depending upon the content of raw xcm, it might be the case that it is not fully revertible
257
        // (i.e xcm that sends a message in another container chain and then return an error).
258
        // Another reason we are not returning error here as otherwise the tx will be reverted and assets will be in limbo in ethereum.
259
        // By returning success here, the assets will be trapped here and claimable by the claimer.
260
16
        let execution_result = execute_xcm::<T, XcmProcessor, XcmWeigher>(
261
16
            eth_location_reanchored_to_tanssi,
262
16
            MaxXcmWeight::get(),
263
16
            prepared_xcm,
264
        );
265

            
266
16
        match execution_result {
267
16
            Ok(outcome) => match outcome {
268
16
                Outcome::Complete { used } => Ok(Some(used)),
269
                Outcome::Incomplete {
270
                    used,
271
                    error: instruction_error,
272
                } => {
273
                    log::error!(
274
                        "Error while executing xcm in raw message processor: {:?}",
275
                        instruction_error
276
                    );
277
                    Ok(Some(used))
278
                }
279
                // This branch will never be executed since current xcm executor implementation only returns
280
                // this Outcome when the weight limit is less than required which is already checked earlier
281
                // in execute_xcm.
282
                Outcome::Error(instruction_error) => {
283
                    log::error!(
284
                        "Error while starting xcm execution in raw message processor: {:?}",
285
                        instruction_error
286
                    );
287
                    Ok(Some(Weight::zero()))
288
                }
289
            },
290
            Err(instruction_error) => {
291
                log::error!(
292
                    "Error while estimating weight for xcm execution in raw message processor: {:?}",
293
                    instruction_error
294
                );
295
                Ok(Some(Weight::zero()))
296
            }
297
        }
298
16
    }
299

            
300
    fn worst_case_message_processor_weight() -> Weight {
301
        MaxXcmWeight::get()
302
    }
303
}