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::traits::Get;
25
use pallet_xcm::Event;
26
use snowbridge_inbound_queue_primitives::v2::{Message, MessageProcessorError};
27
use sp_core::H160;
28
use sp_runtime::DispatchError;
29
use sp_runtime::Weight;
30
use xcm::latest::{ExecuteXcm, InteriorLocation, NetworkId, Outcome, Xcm};
31
use xcm_executor::traits::WeightBounds;
32

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

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

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

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

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

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

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

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