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, 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
>(
53
    PhantomData<(
54
        T,
55
        GatewayAddress,
56
        DefaultClaimer,
57
        EthereumNetwork,
58
        EthereumUniversalLocation,
59
        TanssiUniversalLocation,
60
        XcmProcessor,
61
        XcmWeigher,
62
    )>,
63
);
64

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

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

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

            
150
        // Depending upon the content of raw xcm, it might be the case that it is not fully revertible
151
        // (i.e xcm that sends a message in another container chain and then return an error).
152
        // Another reason we are not returning error here as otherwise the tx will be reverted and assets will be in limbo in ethereum.
153
        // By returning success here, the assets will be trapped here and claimable by the claimer.
154
2
        if let Err(instruction_error) = execute_xcm::<T, XcmProcessor, XcmWeigher>(
155
2
            eth_location_reanchored_to_tanssi,
156
2
            prepared_xcm,
157
2
        ) {
158
            log::error!(
159
                "Error while executing xcm in fallback message processor: {:?}",
160
                instruction_error
161
            );
162
2
        }
163

            
164
        // TODO: Add proper consumed weight
165
2
        Ok(None)
166
2
    }
167
}
168

            
169
/// Conditional fallback processor for privileged protocol messages like (Symbiotic, LayerZero).
170
///
171
/// This is a wrapper around `AssetTrapFallbackProcessor` that only attempts to trap
172
/// assets if the message contains any assets, ETH value, or execution fees.
173
///
174
/// The processor makes the following assumptions:
175
/// - If origin is not gateway proxy: user mistakenly or maliciously sent a privileged message
176
///   → Trap assets for recovery
177
/// - If origin is gateway proxy: privileged sender sent a message with incorrect semantics
178
///   → Return error to signal the problem
179
///
180
/// This conditional behavior prevents unnecessary asset trapping for genuinely invalid
181
/// privileged messages while still protecting user funds in case of errors.
182
pub struct PrivilegedFallbackProcessor<
183
    T,
184
    AssetTrapFallbackProcessor,
185
    GatewayAddress,
186
    DefaultClaimer,
187
    EthereumNetwork,
188
    EthereumUniversalLocation,
189
    TanssiUniversalLocation,
190
    XcmProcessor,
191
    XcmWeigher,
192
>(
193
    PhantomData<(
194
        T,
195
        AssetTrapFallbackProcessor,
196
        GatewayAddress,
197
        DefaultClaimer,
198
        EthereumNetwork,
199
        EthereumUniversalLocation,
200
        TanssiUniversalLocation,
201
        XcmProcessor,
202
        XcmWeigher,
203
    )>,
204
);
205

            
206
impl<
207
        T,
208
        AssetTrapFallbackProcessor,
209
        AccountId,
210
        GatewayAddress,
211
        DefaultClaimer,
212
        EthereumNetwork,
213
        EthereumUniversalLocation,
214
        TanssiUniversalLocation,
215
        XcmProcessor,
216
        XcmWeigher,
217
    > FallbackMessageProcessor<AccountId>
218
    for PrivilegedFallbackProcessor<
219
        T,
220
        AssetTrapFallbackProcessor,
221
        GatewayAddress,
222
        DefaultClaimer,
223
        EthereumNetwork,
224
        EthereumUniversalLocation,
225
        TanssiUniversalLocation,
226
        XcmProcessor,
227
        XcmWeigher,
228
    >
229
where
230
    T: snowbridge_pallet_inbound_queue::Config
231
        + pallet_xcm::Config
232
        + snowbridge_pallet_system::Config,
233
    AssetTrapFallbackProcessor: FallbackMessageProcessor<AccountId>,
234
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
235
    GatewayAddress: Get<H160>,
236
    DefaultClaimer: Get<<T as frame_system::Config>::AccountId>,
237
    EthereumNetwork: Get<NetworkId>,
238
    EthereumUniversalLocation: Get<InteriorLocation>,
239
    TanssiUniversalLocation: Get<InteriorLocation>,
240
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
241
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
242
{
243
3
    fn handle_message(
244
3
        who: AccountId,
245
3
        message: Message,
246
3
    ) -> Result<Option<Weight>, MessageProcessorError> {
247
        // If origin is not gateway proxy, a user mistakenly or maliciously sent privileged message
248
        // If origin is gateway proxy, the privileged middleware sent the message with wrong semantics
249
        // Based on above assumption we do conditional fallback
250
3
        if message.origin != GatewayAddress::get() {
251
1
            AssetTrapFallbackProcessor::handle_message(who, message)
252
        } else {
253
2
            Err(MessageProcessorError::ProcessMessage(DispatchError::Other(
254
2
                "Invalid privileged message payload",
255
2
            )))
256
        }
257
3
    }
258
}