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

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

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

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

            
54
pub mod weights;
55

            
56
use {
57
    frame_support::{
58
        pallet_prelude::*,
59
        traits::{
60
            fungible::{self, Inspect, Mutate},
61
            tokens::Preservation,
62
            Get,
63
        },
64
    },
65
    frame_system::pallet_prelude::*,
66
    snowbridge_core::{AgentId, ChannelId, ParaId, TokenId},
67
    snowbridge_outbound_queue_primitives::v1::{
68
        Command as SnowbridgeCommand, Message as SnowbridgeMessage, SendMessage,
69
    },
70
    snowbridge_outbound_queue_primitives::SendError,
71
    sp_core::{H160, H256},
72
    sp_runtime::{traits::MaybeEquivalence, DispatchResult},
73
    sp_std::vec,
74
    tp_bridge::{ChannelInfo, EthereumSystemChannelManager, TicketInfo},
75
    xcm::prelude::*,
76
};
77

            
78
#[cfg(feature = "runtime-benchmarks")]
79
use tp_bridge::TokenChannelSetterBenchmarkHelperTrait;
80

            
81
pub use pallet::*;
82

            
83
pub type BalanceOf<T> =
84
    <<T as pallet::Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
85

            
86
#[frame_support::pallet]
87
pub mod pallet {
88
    use super::*;
89
    pub use crate::weights::WeightInfo;
90

            
91
    /// The current storage version.
92
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
93

            
94
    pub type RewardPoints = u32;
95
    pub type EraIndex = u32;
96

            
97
    #[pallet::config]
98
    pub trait Config: frame_system::Config {
99
        /// Overarching event type.
100
        type RuntimeEvent: From<Event<Self>>
101
            + IsType<<Self as frame_system::Config>::RuntimeEvent>
102
            + TryInto<Event<Self>>;
103

            
104
        /// Currency to handle fees and internal native transfers.
105
        type Currency: fungible::Inspect<Self::AccountId, Balance: From<u128>>
106
            + fungible::Mutate<Self::AccountId>;
107

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

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

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

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

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

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

            
126
        /// The weight information of this pallet.
127
        type WeightInfo: WeightInfo;
128

            
129
        #[cfg(feature = "runtime-benchmarks")]
130
        type BenchmarkHelper: TokenChannelSetterBenchmarkHelperTrait;
131
    }
132

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

            
151
    // Errors
152
    #[pallet::error]
153
    pub enum Error<T> {
154
        /// The channel's information has not been set on this pallet yet.
155
        ChannelInfoNotSet,
156
        /// Conversion from Location to TokenId failed.
157
        UnknownLocationForToken,
158
        /// The outbound message is invalid prior to send.
159
        InvalidMessage(SendError),
160
        /// The outbound message could not be sent.
161
        TransferMessageNotSent(SendError),
162
    }
163

            
164
329
    #[pallet::pallet]
165
    #[pallet::storage_version(STORAGE_VERSION)]
166
    pub struct Pallet<T>(_);
167

            
168
    // Storage
169
186
    #[pallet::storage]
170
    #[pallet::getter(fn current_channel_info)]
171
    pub type CurrentChannelInfo<T: Config> = StorageValue<_, ChannelInfo, OptionQuery>;
172

            
173
    // Calls
174
    #[pallet::call]
175
    impl<T: Config> Pallet<T> {
176
        #[pallet::call_index(0)]
177
        #[pallet::weight(T::WeightInfo::set_token_transfer_channel())]
178
        pub fn set_token_transfer_channel(
179
            origin: OriginFor<T>,
180
            channel_id: ChannelId,
181
            agent_id: AgentId,
182
            para_id: ParaId,
183
40
        ) -> DispatchResult {
184
40
            ensure_root(origin)?;
185

            
186
39
            let channel_info =
187
39
                T::EthereumSystemHandler::create_channel(channel_id, agent_id, para_id);
188
39

            
189
39
            CurrentChannelInfo::<T>::put(channel_info.clone());
190
39

            
191
39
            Self::deposit_event(Event::<T>::ChannelInfoSet { channel_info });
192
39

            
193
39
            Ok(())
194
        }
195

            
196
        #[pallet::call_index(1)]
197
        #[pallet::weight(T::WeightInfo::transfer_native_token())]
198
        pub fn transfer_native_token(
199
            origin: OriginFor<T>,
200
            amount: u128,
201
            recipient: H160,
202
14
        ) -> DispatchResult {
203
14
            let source = ensure_signed(origin)?;
204

            
205
11
            let channel_info =
206
14
                CurrentChannelInfo::<T>::get().ok_or(Error::<T>::ChannelInfoNotSet)?;
207

            
208
11
            let token_location = T::TokenLocationReanchored::get();
209
11
            let token_id = T::TokenIdFromLocation::convert_back(&token_location)
210
11
                .ok_or(Error::<T>::UnknownLocationForToken)?;
211

            
212
9
            let command = SnowbridgeCommand::MintForeignToken {
213
9
                token_id,
214
9
                recipient,
215
9
                amount,
216
9
            };
217
9

            
218
9
            let message = SnowbridgeMessage {
219
9
                id: None,
220
9
                channel_id: channel_info.channel_id,
221
9
                command,
222
9
            };
223

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

            
227
            // Transfer fees to FeesAccount.
228
9
            T::Currency::transfer(
229
9
                &source,
230
9
                &T::FeesAccount::get(),
231
9
                fee.total(),
232
9
                Preservation::Preserve,
233
9
            )?;
234

            
235
            // Transfer amount to Ethereum's sovereign account.
236
9
            T::Currency::transfer(
237
9
                &source,
238
9
                &T::EthereumSovereignAccount::get(),
239
9
                amount.into(),
240
9
                Preservation::Preserve,
241
9
            )?;
242

            
243
7
            let message_id = ticket.message_id();
244
7

            
245
7
            T::OutboundQueue::deliver(ticket)
246
7
                .map_err(|err| Error::<T>::TransferMessageNotSent(err))?;
247

            
248
7
            Self::deposit_event(Event::<T>::NativeTokenTransferred {
249
7
                message_id,
250
7
                channel_id: channel_info.channel_id,
251
7
                source,
252
7
                recipient,
253
7
                token_id,
254
7
                amount,
255
7
                fee: fee.total(),
256
7
            });
257
7

            
258
7
            Ok(())
259
        }
260
    }
261
}