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
//! EthereumTokenTransfers pallet.
18
//!
19
//! This pallet takes care of sending the native Starlight token from Starlight to Ethereum.
20
//!
21
//! It does this by sending a MintForeignToken command to Ethereum through a
22
//! specific channel_id (which is also stored in this pallet).
23
//!
24
//! ## Extrinsics:
25
//!
26
//! ### set_token_transfer_channel:
27
//!
28
//! Only callable by root. Used to specify which channel_id
29
//! will be used to send the tokens through. It also receives the para_id and
30
//! agent_id params corresponding to the channel specified.
31
//!
32
//! ### transfer_native_token:
33
//!
34
//! Used to perform the actual sending of the tokens, it requires to specify an amount and a recipient,
35
//! which is a H160 account on the Ethereum side.
36
//!
37
//! Inside it, the message is built using the MintForeignToken command. Once the message is validated,
38
//! the amount is transferred from the caller to the EthereumSovereignAccount. This allows to prevent
39
//! double-spending and to track how much of the native token is sent to Ethereum.
40
//!
41
//! After that, the message is delivered to Ethereum through the T::OutboundQueue implementation.
42

            
43
#![cfg_attr(not(feature = "std"), no_std)]
44
extern crate alloc;
45

            
46
#[cfg(test)]
47
mod mock;
48

            
49
#[cfg(test)]
50
mod tests;
51

            
52
#[cfg(feature = "runtime-benchmarks")]
53
mod benchmarking;
54

            
55
pub mod weights;
56

            
57
pub mod origins;
58

            
59
use {
60
    alloc::{vec, vec::Vec},
61
    frame_support::{
62
        pallet_prelude::*,
63
        traits::{
64
            fungible::{self, Inspect, Mutate},
65
            tokens::Preservation,
66
            Get,
67
        },
68
    },
69
    frame_system::{pallet_prelude::*, unique},
70
    snowbridge_core::{reward::MessageId, AgentId, ChannelId, ParaId, TokenId},
71
    snowbridge_outbound_queue_primitives::v1::{
72
        Command as SnowbridgeCommand, Message as SnowbridgeMessage, SendMessage,
73
    },
74
    snowbridge_outbound_queue_primitives::v2::{
75
        Command as SnowbridgeCommandV2, Message as SnowbridgeMessageV2,
76
        SendMessage as SendMessageV2,
77
    },
78
    snowbridge_outbound_queue_primitives::SendError,
79
    sp_core::{H160, H256},
80
    sp_runtime::{
81
        traits::{MaybeEquivalence, TryConvert},
82
        DispatchResult,
83
    },
84
    tp_bridge::{ChannelInfo, ConvertLocation, EthereumSystemChannelManager, TicketInfo},
85
    xcm::prelude::*,
86
};
87

            
88
#[cfg(feature = "runtime-benchmarks")]
89
use tp_bridge::TokenChannelSetterBenchmarkHelperTrait;
90

            
91
pub use pallet::*;
92

            
93
pub type BalanceOf<T> =
94
    <<T as pallet::Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
95

            
96
#[frame_support::pallet]
97
pub mod pallet {
98
    use super::*;
99
    pub use crate::weights::WeightInfo;
100
    use frame_system::Config as SysConfig;
101
    /// The current storage version.
102
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
103

            
104
    pub type RewardPoints = u32;
105
    pub type EraIndex = u32;
106

            
107
    #[pallet::config]
108
    pub trait Config: frame_system::Config {
109
        /// Currency to handle fees and internal native transfers.
110
        type Currency: fungible::Inspect<Self::AccountId, Balance: From<u128>>
111
            + fungible::Mutate<Self::AccountId>;
112

            
113
        /// Validate and send a message to Ethereum.
114
        type OutboundQueue: SendMessage<Balance = BalanceOf<Self>, Ticket: TicketInfo>;
115

            
116
        /// Validate and send a message to Ethereum V2.
117
        type OutboundQueueV2: SendMessageV2<Ticket: TicketInfo>;
118

            
119
        /// Should use v2
120
        type ShouldUseV2: Get<bool>;
121

            
122
        /// Handler for EthereumSystem pallet. Commonly used to manage channel creation.
123
        type EthereumSystemHandler: EthereumSystemChannelManager;
124

            
125
        /// Ethereum sovereign account, where native transfers will go to.
126
        type EthereumSovereignAccount: Get<Self::AccountId>;
127

            
128
        /// Account in which fees will be minted.
129
        type FeesAccount: Get<Self::AccountId>;
130

            
131
        /// Token Location from the external chain's point of view.
132
        type TokenLocationReanchored: Get<Location>;
133

            
134
        /// How to convert from a given Location to a specific TokenId.
135
        type TokenIdFromLocation: MaybeEquivalence<TokenId, Location>;
136

            
137
        /// Converts Location to H256
138
        type LocationHashOf: ConvertLocation<H256>;
139

            
140
        // The bridges configured Ethereum location
141
        type EthereumLocation: Get<Location>;
142

            
143
        /// Means of converting a runtime origin to location
144
        /// Necessary to build the origin in the v2 message
145
        type OriginToLocation: TryConvert<<Self as SysConfig>::RuntimeOrigin, Location>;
146

            
147
        /// This chain's Universal Location.
148
        type UniversalLocation: Get<InteriorLocation>;
149

            
150
        /// The minimum reward for v2 transfers
151
        type MinV2Reward: Get<u128>;
152

            
153
        /// The weight information of this pallet.
154
        type WeightInfo: WeightInfo;
155

            
156
        #[cfg(feature = "runtime-benchmarks")]
157
        type BenchmarkHelper: TokenChannelSetterBenchmarkHelperTrait;
158
        /// Tip Handler which is used for adding tips to the EthereumSystemV2 transaction.
159
        type TipHandler: TipHandler<Self::PalletOrigin>;
160
        type PalletOrigin: From<Origin<Self>>;
161
    }
162

            
163
    pub trait TipHandler<Origin> {
164
        fn add_tip(origin: Origin, message_id: MessageId, amount: u128) -> DispatchResult;
165
        #[cfg(feature = "runtime-benchmarks")]
166
        fn set_tip(_origin: Origin, _message_id: MessageId, _amount: u128) {}
167
    }
168

            
169
    // Events
170
    #[pallet::event]
171
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
172
    pub enum Event<T: Config> {
173
        /// Information for the channel was set properly.
174
        ChannelInfoSet { channel_info: ChannelInfo },
175
        /// Some native token was successfully transferred to Ethereum.
176
        NativeTokenTransferred {
177
            message_id: H256,
178
            channel_id: ChannelId,
179
            source: T::AccountId,
180
            recipient: H160,
181
            token_id: H256,
182
            amount: u128,
183
            fee: BalanceOf<T>,
184
        },
185
    }
186

            
187
    // Errors
188
    #[pallet::error]
189
    pub enum Error<T> {
190
        /// The channel's information has not been set on this pallet yet.
191
        ChannelInfoNotSet,
192
        /// Conversion from Location to TokenId failed.
193
        UnknownLocationForToken,
194
        /// The outbound message is invalid prior to send.
195
        InvalidMessage(SendError),
196
        /// The outbound message could not be sent.
197
        TransferMessageNotSent(SendError),
198
        /// When add_tip extrinsic could not be called.
199
        TipFailed,
200
        V2SendingIsNotAllowed,
201
        TooManyCommands,
202
        OriginConversionFailed,
203
        LocationToOriginConversionFailed,
204
        LocationReanchorFailed,
205
        MinV2RewardNotAchieved,
206
    }
207

            
208
    #[pallet::pallet]
209
    #[pallet::storage_version(STORAGE_VERSION)]
210
    pub struct Pallet<T>(_);
211

            
212
    // Storage
213
    #[pallet::storage]
214
    #[pallet::getter(fn current_channel_info)]
215
    pub type CurrentChannelInfo<T: Config> = StorageValue<_, ChannelInfo, OptionQuery>;
216

            
217
    #[derive(
218
        PartialEq,
219
        Eq,
220
        Clone,
221
        MaxEncodedLen,
222
        Encode,
223
        Decode,
224
        DecodeWithMemTracking,
225
        TypeInfo,
226
        RuntimeDebug,
227
    )]
228
    #[pallet::origin]
229
    pub enum Origin<T: Config> {
230
        /// The origin for the pallet to make extrinsics.
231
        EthereumTokenTransfers(T::AccountId),
232
    }
233

            
234
    // Calls
235
    #[pallet::call]
236
    impl<T: Config> Pallet<T> {
237
        #[pallet::call_index(0)]
238
        #[pallet::weight(T::WeightInfo::set_token_transfer_channel())]
239
        pub fn set_token_transfer_channel(
240
            origin: OriginFor<T>,
241
            channel_id: ChannelId,
242
            agent_id: AgentId,
243
            para_id: ParaId,
244
87
        ) -> DispatchResult {
245
87
            ensure_root(origin)?;
246

            
247
86
            let channel_info =
248
86
                T::EthereumSystemHandler::create_channel(channel_id, agent_id, para_id);
249

            
250
86
            CurrentChannelInfo::<T>::put(channel_info.clone());
251

            
252
86
            Self::deposit_event(Event::<T>::ChannelInfoSet { channel_info });
253

            
254
86
            Ok(())
255
        }
256

            
257
        #[pallet::call_index(1)]
258
        #[pallet::weight(T::WeightInfo::transfer_native_token())]
259
        pub fn transfer_native_token(
260
            origin: OriginFor<T>,
261
            amount: u128,
262
            recipient: H160,
263
14
        ) -> DispatchResult {
264
14
            let source = ensure_signed(origin)?;
265

            
266
11
            let channel_info =
267
14
                CurrentChannelInfo::<T>::get().ok_or(Error::<T>::ChannelInfoNotSet)?;
268

            
269
11
            let token_location = T::TokenLocationReanchored::get();
270
11
            let token_id = T::TokenIdFromLocation::convert_back(&token_location)
271
11
                .ok_or(Error::<T>::UnknownLocationForToken)?;
272

            
273
9
            let command = SnowbridgeCommand::MintForeignToken {
274
9
                token_id,
275
9
                recipient,
276
9
                amount,
277
9
            };
278

            
279
9
            let message = SnowbridgeMessage {
280
9
                id: None,
281
9
                channel_id: channel_info.channel_id,
282
9
                command,
283
9
            };
284

            
285
9
            let (ticket, fee) = T::OutboundQueue::validate(&message)
286
9
                .map_err(|err| Error::<T>::InvalidMessage(err))?;
287

            
288
            // Transfer fees to FeesAccount.
289
9
            T::Currency::transfer(
290
9
                &source,
291
9
                &T::FeesAccount::get(),
292
9
                fee.total(),
293
9
                Preservation::Preserve,
294
            )?;
295

            
296
            // Transfer amount to Ethereum's sovereign account.
297
9
            T::Currency::transfer(
298
9
                &source,
299
9
                &T::EthereumSovereignAccount::get(),
300
9
                amount.into(),
301
9
                Preservation::Preserve,
302
2
            )?;
303

            
304
7
            let message_id = ticket.message_id();
305

            
306
7
            T::OutboundQueue::deliver(ticket)
307
7
                .map_err(|err| Error::<T>::TransferMessageNotSent(err))?;
308

            
309
7
            Self::deposit_event(Event::<T>::NativeTokenTransferred {
310
7
                message_id,
311
7
                channel_id: channel_info.channel_id,
312
7
                source,
313
7
                recipient,
314
7
                token_id,
315
7
                amount,
316
7
                fee: fee.total(),
317
7
            });
318

            
319
7
            Ok(())
320
        }
321

            
322
        #[pallet::call_index(2)]
323
        #[pallet::weight(T::WeightInfo::transfer_native_token())]
324
        pub fn transfer_native_token_v2(
325
            origin: OriginFor<T>,
326
            amount: u128,
327
            recipient: H160,
328
            reward: u128,
329
5
        ) -> DispatchResult {
330
5
            let source = ensure_signed(origin.clone())?;
331
5
            let origin_location = T::OriginToLocation::try_convert(origin)
332
5
                .map_err(|_| Error::<T>::OriginConversionFailed)?;
333
5
            let origin = Self::location_to_message_origin(origin_location)?;
334

            
335
4
            ensure!(T::ShouldUseV2::get(), Error::<T>::V2SendingIsNotAllowed);
336

            
337
            // Check for minimum fee
338
3
            ensure!(
339
3
                reward >= T::MinV2Reward::get(),
340
1
                Error::<T>::MinV2RewardNotAchieved
341
            );
342

            
343
2
            let channel_info =
344
2
                CurrentChannelInfo::<T>::get().ok_or(Error::<T>::ChannelInfoNotSet)?;
345

            
346
2
            let token_location = T::TokenLocationReanchored::get();
347
2
            let token_id = T::TokenIdFromLocation::convert_back(&token_location)
348
2
                .ok_or(Error::<T>::UnknownLocationForToken)?;
349

            
350
            // Transfer amount to Ethereum's sovereign account.
351
2
            T::Currency::transfer(
352
2
                &source,
353
2
                &T::EthereumSovereignAccount::get(),
354
2
                amount.into(),
355
2
                Preservation::Preserve,
356
            )?;
357

            
358
            // Transfer fee to fee's account.
359
2
            T::Currency::transfer(
360
2
                &source,
361
2
                &T::FeesAccount::get(),
362
2
                reward.into(),
363
2
                Preservation::Preserve,
364
1
            )?;
365

            
366
1
            let command = SnowbridgeCommandV2::MintForeignToken {
367
1
                token_id,
368
1
                recipient,
369
1
                amount,
370
1
            };
371

            
372
1
            let id = unique((origin, &command)).into();
373
1
            let mut commands: Vec<SnowbridgeCommandV2> = Vec::new();
374
1
            commands.push(command);
375

            
376
1
            let message = SnowbridgeMessageV2 {
377
1
                id,
378
1
                commands: BoundedVec::try_from(commands)
379
1
                    .map_err(|_| Error::<T>::TooManyCommands)?,
380
1
                fee: reward,
381
1
                origin,
382
            };
383

            
384
1
            let ticket = T::OutboundQueueV2::validate(&message)
385
1
                .map_err(|err| Error::<T>::InvalidMessage(err))?;
386
1
            let message_id = ticket.message_id();
387

            
388
1
            T::OutboundQueueV2::deliver(ticket)
389
1
                .map_err(|err| Error::<T>::TransferMessageNotSent(err))?;
390

            
391
1
            Self::deposit_event(Event::<T>::NativeTokenTransferred {
392
1
                message_id,
393
1
                channel_id: channel_info.channel_id,
394
1
                source,
395
1
                recipient,
396
1
                token_id,
397
1
                amount,
398
1
                fee: reward.into(),
399
1
            });
400
1
            Ok(())
401
        }
402

            
403
        #[pallet::call_index(3)]
404
        #[pallet::weight(T::WeightInfo::add_tip())]
405
        pub fn add_tip(
406
            origin: OriginFor<T>,
407
            message_id: MessageId,
408
            amount: u128,
409
9
        ) -> DispatchResult {
410
9
            let sender = ensure_signed(origin)?;
411

            
412
9
            let custom_origin =
413
9
                T::PalletOrigin::from(Origin::<T>::EthereumTokenTransfers(sender.clone()));
414

            
415
9
            T::TipHandler::add_tip(custom_origin, message_id.clone(), amount)
416
9
                .map_err(|_| Error::<T>::TipFailed)?;
417

            
418
6
            Ok(())
419
        }
420
    }
421

            
422
    impl<T: Config> Pallet<T> {
423
5
        pub fn location_to_message_origin(location: Location) -> Result<H256, Error<T>> {
424
5
            let reanchored_location = Self::reanchor(location)?;
425
5
            T::LocationHashOf::convert_location(&reanchored_location)
426
5
                .ok_or(Error::<T>::LocationToOriginConversionFailed)
427
5
        }
428
        /// Reanchor the `location` in context of ethereum
429
5
        pub fn reanchor(location: Location) -> Result<Location, Error<T>> {
430
5
            location
431
5
                .reanchored(&T::EthereumLocation::get(), &T::UniversalLocation::get())
432
5
                .map_err(|_| Error::<T>::LocationReanchorFailed)
433
5
        }
434
    }
435
}
436

            
437
pub struct DenyTipHandler<T>(core::marker::PhantomData<T>);
438

            
439
impl<T, Origin> TipHandler<Origin> for DenyTipHandler<T> {
440
    #[cfg(not(feature = "runtime-benchmarks"))]
441
1
    fn add_tip(_origin: Origin, _message_id: MessageId, _amount: u128) -> DispatchResult {
442
1
        Err("Execution is not permitted!".into())
443
1
    }
444
    // in order for the extrinsic to still be benchmarkable, we implement it empty
445
    #[cfg(feature = "runtime-benchmarks")]
446
    fn add_tip(_origin: Origin, _message_id: MessageId, _amount: u128) -> DispatchResult {
447
        Ok(())
448
    }
449
}