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, reanchor_location_to_tanssi, ExtractedXcmConstructionInfo,
21
    FallbackMessageProcessor,
22
};
23
use core::marker::PhantomData;
24
use frame_support::__private::Get;
25
use snowbridge_inbound_queue_primitives::v2::{Message, MessageProcessorError};
26
use sp_core::H160;
27
use sp_runtime::DispatchError;
28
use sp_runtime::Weight;
29
use xcm::latest::{ExecuteXcm, InteriorLocation, NetworkId, Outcome, Xcm};
30
use xcm_executor::traits::WeightBounds;
31

            
32
/// Fallback message processor that traps assets from failed Snowbridge V2 messages.
33
///
34
/// When a message from Ethereum cannot be processed normally, this processor will:
35
/// 1. Extract assets, ETH value, and execution fees from the failed message
36
/// 2. Prepare XCM instructions to trap these assets in the Tanssi relay chain
37
/// 3. Execute the XCM, making trapped assets claimable by the specified claimer
38
///
39
/// This processor always returns success to prevent reverting the Ethereum transaction,
40
/// which would leave assets in limbo on the Ethereum side. If XCM execution fails,
41
/// the error is logged but the processor still returns success, allowing the claimer
42
/// to recover the trapped assets later.
43
pub struct AssetTrapFallbackProcessor<
44
    T,
45
    GatewayAddress,
46
    DefaultClaimer,
47
    EthereumNetwork,
48
    EthereumUniversalLocation,
49
    TanssiUniversalLocation,
50
    XcmProcessor,
51
    XcmWeigher,
52
    MaxXcmWeight,
53
>(
54
    PhantomData<(
55
        T,
56
        GatewayAddress,
57
        DefaultClaimer,
58
        EthereumNetwork,
59
        EthereumUniversalLocation,
60
        TanssiUniversalLocation,
61
        XcmProcessor,
62
        XcmWeigher,
63
        MaxXcmWeight,
64
    )>,
65
);
66

            
67
impl<
68
        T,
69
        AccountId,
70
        GatewayAddress,
71
        DefaultClaimer,
72
        EthereumNetwork,
73
        EthereumUniversalLocation,
74
        TanssiUniversalLocation,
75
        XcmProcessor,
76
        XcmWeigher,
77
        MaxXcmWeight,
78
    > FallbackMessageProcessor<AccountId>
79
    for AssetTrapFallbackProcessor<
80
        T,
81
        GatewayAddress,
82
        DefaultClaimer,
83
        EthereumNetwork,
84
        EthereumUniversalLocation,
85
        TanssiUniversalLocation,
86
        XcmProcessor,
87
        XcmWeigher,
88
        MaxXcmWeight,
89
    >
90
where
91
    T: snowbridge_pallet_inbound_queue::Config
92
        + pallet_xcm::Config
93
        + snowbridge_pallet_system::Config,
94
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
95
    GatewayAddress: Get<H160>,
96
    DefaultClaimer: Get<<T as frame_system::Config>::AccountId>,
97
    EthereumNetwork: Get<NetworkId>,
98
    EthereumUniversalLocation: Get<InteriorLocation>,
99
    TanssiUniversalLocation: Get<InteriorLocation>,
100
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
101
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
102
    MaxXcmWeight: Get<Weight>,
103
{
104
2
    fn handle_message(
105
2
        _who: AccountId,
106
2
        message: Message,
107
2
    ) -> Result<Option<Weight>, MessageProcessorError> {
108
2
        let extracted_message: ExtractedXcmConstructionInfo<
109
2
            <T as pallet_xcm::Config>::RuntimeCall,
110
2
        > = ExtractedXcmConstructionInfo {
111
2
            origin: message.origin,
112
2
            maybe_claimer: message.claimer.clone(),
113
2
            assets: message.assets.clone(),
114
2
            eth_value: message.value,
115
2
            execution_fee_in_eth: message.execution_fee,
116
2
            nonce: message.nonce,
117
2
            user_xcm: Xcm::new(),
118
2
        };
119

            
120
2
        let prepared_xcm = crate::processors::v2::prepare_raw_message_xcm_instructions::<T>(
121
2
            EthereumNetwork::get(),
122
2
            &EthereumUniversalLocation::get(),
123
2
            &TanssiUniversalLocation::get(),
124
2
            GatewayAddress::get(),
125
2
            DefaultClaimer::get(),
126
2
            crate::processors::v2::RAW_MESSAGE_PROCESSOR_TOPIC_PREFIX,
127
2
            extracted_message,
128
        )
129
2
        .map_err(|location_conversion_error| {
130
            log::error!(
131
                "Error while preparing xcm instructions: {:?}",
132
                location_conversion_error
133
            );
134
            MessageProcessorError::ProcessMessage(DispatchError::Other(
135
                "Error while preparing xcm instructions",
136
            ))
137
        })?
138
2
        .into();
139

            
140
2
        let eth_location_reanchored_to_tanssi = reanchor_location_to_tanssi(
141
2
            &EthereumUniversalLocation::get(),
142
2
            &TanssiUniversalLocation::get(),
143
2
            ().into(),
144
        )
145
2
        .map_err(|location_conversion_error| {
146
            log::error!(
147
                "Unable to reanchor eth location to tanssi: {:?}",
148
                location_conversion_error
149
            );
150
            MessageProcessorError::ProcessMessage(DispatchError::Other(
151
                "Unable to reanchor eth location to tanssi",
152
            ))
153
        })?;
154

            
155
        // Depending upon the content of raw xcm, it might be the case that it is not fully revertible
156
        // (i.e xcm that sends a message in another container chain and then return an error).
157
        // Another reason we are not returning error here as otherwise the tx will be reverted and assets will be in limbo in ethereum.
158
        // By returning success here, the assets will be trapped here and claimable by the claimer.
159
2
        let execution_result = execute_xcm::<T, XcmProcessor, XcmWeigher>(
160
2
            eth_location_reanchored_to_tanssi,
161
2
            MaxXcmWeight::get(),
162
2
            prepared_xcm,
163
        );
164

            
165
2
        match execution_result {
166
2
            Ok(outcome) => match outcome {
167
2
                Outcome::Complete { used } => Ok(Some(used)),
168
                Outcome::Incomplete {
169
                    used,
170
                    error: instruction_error,
171
                } => {
172
                    log::error!(
173
                        "Error while executing xcm in fallback message processor: {:?}",
174
                        instruction_error
175
                    );
176
                    Ok(Some(used))
177
                }
178
                // This branch will never be executed since current xcm executor implementation only returns
179
                // this Outcome when the weight limit is less than required which is already checked earlier
180
                // in execute_xcm.
181
                Outcome::Error(instruction_error) => {
182
                    log::error!(
183
                        "Error while starting xcm execution in fallback message processor: {:?}",
184
                        instruction_error
185
                    );
186
                    Ok(Some(Weight::zero()))
187
                }
188
            },
189
            Err(instruction_error) => {
190
                log::error!(
191
                    "Error while estimating weight for xcm execution in fallback message processor: {:?}",
192
                    instruction_error
193
                );
194
                Ok(Some(Weight::zero()))
195
            }
196
        }
197
2
    }
198
}
199

            
200
/// Conditional fallback processor for Symbiotic protocol messages.
201
///
202
/// This is a wrapper around `AssetTrapFallbackProcessor` that only attempts to trap
203
/// assets if the message contains any assets, ETH value, or execution fees.
204
///
205
/// The processor makes the following assumptions:
206
/// - If origin is not gateway proxy: user mistakenly or maliciously sent a Symbiotic message
207
///   → Trap assets for recovery
208
/// - If origin is gateway proxy: Symbiotic middleware sent a message with incorrect semantics
209
///   → Return error to signal the problem
210
///
211
/// This conditional behavior prevents unnecessary asset trapping for genuinely invalid
212
/// Symbiotic messages while still protecting user funds in case of errors.
213
pub struct SymbioticFallbackProcessor<T, AssetTrapFallbackProcessor, GatewayAddress>(
214
    PhantomData<(T, AssetTrapFallbackProcessor, GatewayAddress)>,
215
);
216

            
217
impl<T, AssetTrapFallbackProcessor, AccountId, GatewayAddress> FallbackMessageProcessor<AccountId>
218
    for SymbioticFallbackProcessor<T, AssetTrapFallbackProcessor, GatewayAddress>
219
where
220
    T: snowbridge_pallet_inbound_queue::Config
221
        + pallet_xcm::Config
222
        + snowbridge_pallet_system::Config,
223
    AssetTrapFallbackProcessor: FallbackMessageProcessor<AccountId>,
224
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
225
    GatewayAddress: Get<H160>,
226
{
227
3
    fn handle_message(
228
3
        who: AccountId,
229
3
        message: Message,
230
3
    ) -> Result<Option<Weight>, MessageProcessorError> {
231
        // If origin is not gateway proxy, a user mistakenly or maliciously sent Symbiotic message
232
        // If origin is gateway proxy, the symbiotic middleware sent the message with wrong semantics
233
        // Based on above assumption we do conditional fallback
234
3
        if message.origin != GatewayAddress::get() {
235
1
            AssetTrapFallbackProcessor::handle_message(who, message)
236
        } else {
237
2
            Err(MessageProcessorError::ProcessMessage(DispatchError::Other(
238
2
                "Invalid symbiotic message payload",
239
2
            )))
240
        }
241
3
    }
242
}