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,
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::*,
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::SendError,
75
    sp_core::{H160, H256},
76
    sp_runtime::{traits::MaybeEquivalence, DispatchResult},
77
    tp_bridge::{ChannelInfo, EthereumSystemChannelManager, TicketInfo},
78
    xcm::prelude::*,
79
};
80

            
81
#[cfg(feature = "runtime-benchmarks")]
82
use tp_bridge::TokenChannelSetterBenchmarkHelperTrait;
83

            
84
pub use pallet::*;
85

            
86
pub type BalanceOf<T> =
87
    <<T as pallet::Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
88

            
89
#[frame_support::pallet]
90
pub mod pallet {
91
    use super::*;
92
    pub use crate::weights::WeightInfo;
93

            
94
    /// The current storage version.
95
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
96

            
97
    pub type RewardPoints = u32;
98
    pub type EraIndex = u32;
99

            
100
    #[pallet::config]
101
    pub trait Config: frame_system::Config {
102
        /// Currency to handle fees and internal native transfers.
103
        type Currency: fungible::Inspect<Self::AccountId, Balance: From<u128>>
104
            + fungible::Mutate<Self::AccountId>;
105

            
106
        /// Validate and send a message to Ethereum.
107
        type OutboundQueue: SendMessage<Balance = BalanceOf<Self>, Ticket: TicketInfo>;
108

            
109
        /// Handler for EthereumSystem pallet. Commonly used to manage channel creation.
110
        type EthereumSystemHandler: EthereumSystemChannelManager;
111

            
112
        /// Ethereum sovereign account, where native transfers will go to.
113
        type EthereumSovereignAccount: Get<Self::AccountId>;
114

            
115
        /// Account in which fees will be minted.
116
        type FeesAccount: Get<Self::AccountId>;
117

            
118
        /// Token Location from the external chain's point of view.
119
        type TokenLocationReanchored: Get<Location>;
120

            
121
        /// How to convert from a given Location to a specific TokenId.
122
        type TokenIdFromLocation: MaybeEquivalence<TokenId, Location>;
123

            
124
        /// The weight information of this pallet.
125
        type WeightInfo: WeightInfo;
126

            
127
        #[cfg(feature = "runtime-benchmarks")]
128
        type BenchmarkHelper: TokenChannelSetterBenchmarkHelperTrait;
129
        /// Tip Handler which is used for adding tips to the EthereumSystemV2 transaction.
130
        type TipHandler: TipHandler<Self::PalletOrigin>;
131
        type PalletOrigin: From<Origin<Self>>;
132
    }
133

            
134
    pub trait TipHandler<Origin> {
135
        fn add_tip(origin: Origin, message_id: MessageId, amount: u128) -> DispatchResult;
136
        #[cfg(feature = "runtime-benchmarks")]
137
        fn set_tip(_origin: Origin, _message_id: MessageId, _amount: u128) {}
138
    }
139

            
140
    // Events
141
    #[pallet::event]
142
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
143
    pub enum Event<T: Config> {
144
        /// Information for the channel was set properly.
145
        ChannelInfoSet { channel_info: ChannelInfo },
146
        /// Some native token was successfully transferred to Ethereum.
147
        NativeTokenTransferred {
148
            message_id: H256,
149
            channel_id: ChannelId,
150
            source: T::AccountId,
151
            recipient: H160,
152
            token_id: H256,
153
            amount: u128,
154
            fee: BalanceOf<T>,
155
        },
156
    }
157

            
158
    // Errors
159
    #[pallet::error]
160
    pub enum Error<T> {
161
        /// The channel's information has not been set on this pallet yet.
162
        ChannelInfoNotSet,
163
        /// Conversion from Location to TokenId failed.
164
        UnknownLocationForToken,
165
        /// The outbound message is invalid prior to send.
166
        InvalidMessage(SendError),
167
        /// The outbound message could not be sent.
168
        TransferMessageNotSent(SendError),
169
        /// When add_tip extrinsic could not be called.
170
        TipFailed,
171
    }
172

            
173
    #[pallet::pallet]
174
    #[pallet::storage_version(STORAGE_VERSION)]
175
    pub struct Pallet<T>(_);
176

            
177
    // Storage
178
    #[pallet::storage]
179
    #[pallet::getter(fn current_channel_info)]
180
    pub type CurrentChannelInfo<T: Config> = StorageValue<_, ChannelInfo, OptionQuery>;
181

            
182
    #[derive(
183
        PartialEq,
184
        Eq,
185
        Clone,
186
        MaxEncodedLen,
187
        Encode,
188
        Decode,
189
        DecodeWithMemTracking,
190
        TypeInfo,
191
        RuntimeDebug,
192
    )]
193
    #[pallet::origin]
194
    pub enum Origin<T: Config> {
195
        /// The origin for the pallet to make extrinsics.
196
        EthereumTokenTransfers(T::AccountId),
197
    }
198

            
199
    // Calls
200
    #[pallet::call]
201
    impl<T: Config> Pallet<T> {
202
        #[pallet::call_index(0)]
203
        #[pallet::weight(T::WeightInfo::set_token_transfer_channel())]
204
        pub fn set_token_transfer_channel(
205
            origin: OriginFor<T>,
206
            channel_id: ChannelId,
207
            agent_id: AgentId,
208
            para_id: ParaId,
209
82
        ) -> DispatchResult {
210
82
            ensure_root(origin)?;
211

            
212
81
            let channel_info =
213
81
                T::EthereumSystemHandler::create_channel(channel_id, agent_id, para_id);
214

            
215
81
            CurrentChannelInfo::<T>::put(channel_info.clone());
216

            
217
81
            Self::deposit_event(Event::<T>::ChannelInfoSet { channel_info });
218

            
219
81
            Ok(())
220
        }
221

            
222
        #[pallet::call_index(1)]
223
        #[pallet::weight(T::WeightInfo::transfer_native_token())]
224
        pub fn transfer_native_token(
225
            origin: OriginFor<T>,
226
            amount: u128,
227
            recipient: H160,
228
14
        ) -> DispatchResult {
229
14
            let source = ensure_signed(origin)?;
230

            
231
11
            let channel_info =
232
14
                CurrentChannelInfo::<T>::get().ok_or(Error::<T>::ChannelInfoNotSet)?;
233

            
234
11
            let token_location = T::TokenLocationReanchored::get();
235
11
            let token_id = T::TokenIdFromLocation::convert_back(&token_location)
236
11
                .ok_or(Error::<T>::UnknownLocationForToken)?;
237

            
238
9
            let command = SnowbridgeCommand::MintForeignToken {
239
9
                token_id,
240
9
                recipient,
241
9
                amount,
242
9
            };
243

            
244
9
            let message = SnowbridgeMessage {
245
9
                id: None,
246
9
                channel_id: channel_info.channel_id,
247
9
                command,
248
9
            };
249

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

            
253
            // Transfer fees to FeesAccount.
254
9
            T::Currency::transfer(
255
9
                &source,
256
9
                &T::FeesAccount::get(),
257
9
                fee.total(),
258
9
                Preservation::Preserve,
259
            )?;
260

            
261
            // Transfer amount to Ethereum's sovereign account.
262
9
            T::Currency::transfer(
263
9
                &source,
264
9
                &T::EthereumSovereignAccount::get(),
265
9
                amount.into(),
266
9
                Preservation::Preserve,
267
2
            )?;
268

            
269
7
            let message_id = ticket.message_id();
270

            
271
7
            T::OutboundQueue::deliver(ticket)
272
7
                .map_err(|err| Error::<T>::TransferMessageNotSent(err))?;
273

            
274
7
            Self::deposit_event(Event::<T>::NativeTokenTransferred {
275
7
                message_id,
276
7
                channel_id: channel_info.channel_id,
277
7
                source,
278
7
                recipient,
279
7
                token_id,
280
7
                amount,
281
7
                fee: fee.total(),
282
7
            });
283

            
284
7
            Ok(())
285
        }
286

            
287
        #[pallet::call_index(2)]
288
        #[pallet::weight(T::WeightInfo::add_tip())]
289
        pub fn add_tip(
290
            origin: OriginFor<T>,
291
            message_id: MessageId,
292
            amount: u128,
293
8
        ) -> DispatchResult {
294
8
            let sender = ensure_signed(origin)?;
295

            
296
8
            let custom_origin =
297
8
                T::PalletOrigin::from(Origin::<T>::EthereumTokenTransfers(sender.clone()));
298

            
299
8
            T::TipHandler::add_tip(custom_origin, message_id.clone(), amount)
300
8
                .map_err(|_| Error::<T>::TipFailed)?;
301

            
302
5
            Ok(())
303
        }
304
    }
305
}
306

            
307
pub struct DenyTipHandler<T>(core::marker::PhantomData<T>);
308

            
309
impl<T, Origin> TipHandler<Origin> for DenyTipHandler<T> {
310
    #[cfg(not(feature = "runtime-benchmarks"))]
311
1
    fn add_tip(_origin: Origin, _message_id: MessageId, _amount: u128) -> DispatchResult {
312
1
        Err("Execution is not permitted!".into())
313
1
    }
314
    // in order for the extrinsic to still be benchmarkable, we implement it empty
315
    #[cfg(feature = "runtime-benchmarks")]
316
    fn add_tip(_origin: Origin, _message_id: MessageId, _amount: u128) -> DispatchResult {
317
        Ok(())
318
    }
319
}