pallet_native_token_management/lib.rs
1//! Pallet allowing Partner Chains to support movement of their native token from Cardano.
2//!
3//! # Context and purpose of this pallet
4//!
5//! Partner Chains Smart Contracts establish a notion of liquid and illiquid supply of the native
6//! token on Cardano, represented as native tokens being either freely available in user accounts
7//! or locked under a designated illiquid supply address. Movement of native tokens into the illiquid
8//! supply on Cardano signals that an equivalent amount of tokens should be made available on the
9//! Partner Chain.
10//!
11//! This pallet consumes inherent data containing information on the amount of native tokens newly
12//! locked on Cardano and produces an inherent extrinsic to handle their movement. The specific
13//! logic releasing the tokens is left for the Partner Chain developer to implement and is configured
14//! in the pallet via the [TokenTransferHandler] trait.
15//!
16//! *IMPORTANT*: The mechanism implemented by this pallet is only concerned with the amount of tokens
17//! moved and does not attach any metadata to the transfers. In particular it is not a
18//! fully-featured token bridge and needs to be combined with a separate sender-receiver
19//! metadata channel to implement one.
20//!
21//! # Usage
22//!
23//! ## Defining a transfer handler
24//!
25//! The main purpose of the pallet is to trigger user-defined runtime logic whenever a new batch of
26//! tokens is observed to have been locked on Cardano. To do that, the Partner Chain builder should
27//! define a type implementing the [TokenTransferHandler] trait, eg.:
28//!
29//! ```rust
30//! use sidechain_domain::NativeTokenAmount;
31//! use frame_support::pallet_prelude::DispatchResult;
32//!
33//! pub struct TransferHandler;
34//! impl pallet_native_token_management::TokenTransferHandler for TransferHandler {
35//! fn handle_token_transfer(token_amount: NativeTokenAmount) -> DispatchResult {
36//! log::info!("💸 Registered transfer of {} native tokens", token_amount.0);
37//! Ok(())
38//! }
39//! }
40//! ```
41//!
42//! ## Adding to the runtime
43//!
44//! Aside from the transfer handler, the pallet requires minimal runtime configuration: the runtime event
45//! type, origin for governance calls implementing [EnsureOrigin] and weights:
46//!
47//! ```rust,ignore
48//! impl pallet_native_token_management::Config for Runtime {
49//! type RuntimeEvent = RuntimeEvent;
50//! type TokenTransferHandler = TransferHandler;
51//! type MainChainScriptsOrigin = frame_system::EnsureRoot<Self::AccountId>;
52//! type WeightInfo = pallet_native_token_management::weights::SubstrateWeight<Runtime>;
53//! }
54//! ```
55//!
56//! Keep in mind that if the handler logic has to perform storage operations, the pallet's benchmarks
57//! should be rerun. Otherwise default weights are provided in [pallet_native_token_management::weights].
58//!
59//! ## Script configuration
60//!
61//! For token transfers to be observed, the pallet must be configured with correct Cardano addresses and
62//! scripts used to idendify them in the ledger. These scripts can be set in two ways: through genesis
63//! configuration if the pallet is present in the initial runtime of a chain; or via a governance action
64//! using the [set_main_chain_scripts] extrinsic.
65//!
66//! ### Genesis configuratin
67//!
68//! Initial main chain scripts can be set in the genesis configuration, like so:
69//! ```json
70//! {
71//! "nativeTokenManagement": {
72//! "mainChainScripts": {
73//! "illiquid_supply_validator_address": "0x616464725f74657374317772687674767833663067397776397278386b66716336306a7661336530376e71756a6b32637370656b76346d717339726a64767a",
74//! "native_token_asset_name": "0x5043546f6b656e44656d6f",
75//! "native_token_policy_id": "0xada83ddd029614381f00e28de0922ab0dec6983ea9dd29ae20eef9b4"
76//! }
77//! }
78//! }
79//! ```
80//!
81//! Note that the `mainChainScripts` field is optional. If it is left empty, the pallet will stay inactive
82//! until configuration is set later.
83//!
84//! ### Main chain scripts extrinsic
85//!
86//! Once the chain is already started, to set initial main chain scripts to a newly added pallet, or to
87//! change the existing ones, the [set_main_chain_scripts] extrinsic must be submitted through on-chain
88//! governance mechanism like `sudo` or `pallet_democracy`. Who exactly can submit this extrinsic is
89//! controlled by the [Config::MainChainScriptsOrigin] field of the pallet's configuration, but for security
90//! it must be a trusted entity.
91//!
92//! #### Initialization state of the pallet
93//!
94//! The pallet tracks its own initialization state through the [Initialized] storage flag. This information
95//! is necessary for it to correctly observe historical data and the state is reset every time main chain
96//! scripts are changed in the pallet. This allows the Partner Chain governance to switch to new versions
97//! of the smart contracts. However, some consideration must be taken while changing the scripts:
98//! 1. This mechanism can not handle changing the main chain scripts to values that were used before.
99//! Doing so will cause some transfers to be registered again, resulting in potential double-spend.
100//! This means that a script version roll-back is not possible.
101//! 2. Moving funds from an old illiquid supply address to a new one requires unlocking them and re-locking
102//! at the new address, resulting in a new transfer being observed. The logic handling the token movement,
103//! including the transfer handler, must be able to handle this unlock-relock behaviour if a Partner Chain
104//! governance wishes to migrate tokens to the new address.
105//!
106#![cfg_attr(not(feature = "std"), no_std)]
107#![deny(missing_docs)]
108
109use frame_support::pallet_prelude::*;
110use frame_system::pallet_prelude::*;
111pub use pallet::*;
112use sidechain_domain::*;
113use sp_native_token_management::*;
114
115mod benchmarking;
116
117#[cfg(test)]
118mod tests;
119
120#[cfg(test)]
121mod mock;
122
123pub mod weights;
124pub use weights::WeightInfo;
125
126/// Interface for user-provided logic to handle native token transfers into the illiquid supply on the main chain.
127///
128/// The handler will be called with **the total sum** of transfers since the previous partner chain block.
129pub trait TokenTransferHandler {
130 /// New transfer even handler
131 fn handle_token_transfer(token_amount: NativeTokenAmount) -> DispatchResult;
132}
133
134#[frame_support::pallet]
135pub mod pallet {
136 use super::*;
137
138 #[pallet::pallet]
139 pub struct Pallet<T>(_);
140
141 #[pallet::config]
142 pub trait Config: frame_system::Config {
143 /// Even type used by the runtime
144 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
145
146 /// Origin for governance calls
147 type MainChainScriptsOrigin: EnsureOrigin<Self::RuntimeOrigin>;
148
149 /// Event handler for incoming native token transfers
150 type TokenTransferHandler: TokenTransferHandler;
151
152 /// Weight information for this pallet's extrinsics
153 type WeightInfo: WeightInfo;
154 }
155
156 /// Events emitted by this pallet
157 #[pallet::event]
158 #[pallet::generate_deposit(pub(super) fn deposit_event)]
159 pub enum Event<T: Config> {
160 /// Signals that a new native token transfer has been processed by the pallet
161 TokensTransfered(NativeTokenAmount),
162 }
163
164 /// Error type used by the pallet's extrinsics
165 #[pallet::error]
166 pub enum Error<T> {
167 /// Indicates that the inherent was called while there was no main chain scripts set in the
168 /// pallet's storage. This is indicative of a programming bug.
169 CalledWithoutConfiguration,
170 /// Indicates that the inherent was called a second time in the same block
171 TransferAlreadyHandled,
172 }
173
174 #[pallet::storage]
175 pub type MainChainScriptsConfiguration<T: Config> =
176 StorageValue<_, sp_native_token_management::MainChainScripts, OptionQuery>;
177
178 /// Stores the pallet's initialization state.
179 ///
180 /// The pallet is considered initialized if its inherent has been successfuly called at least once since
181 /// genesis or the last invocation of [set_main_chain_scripts][Pallet::set_main_chain_scripts].
182 #[pallet::storage]
183 pub type Initialized<T: Config> = StorageValue<_, bool, ValueQuery>;
184
185 /// Transient storage containing the amount of native token transfer registered in the current block.
186 ///
187 /// Any value in this storage is only present during execution of a block and is emptied on block finalization.
188 #[pallet::storage]
189 pub type TransferedThisBlock<T: Config> = StorageValue<_, NativeTokenAmount, OptionQuery>;
190
191 /// Genesis configuration of the pallet
192 #[pallet::genesis_config]
193 #[derive(frame_support::DefaultNoBound)]
194 pub struct GenesisConfig<T: Config> {
195 /// Initial main chain scripts
196 pub main_chain_scripts: Option<sp_native_token_management::MainChainScripts>,
197 #[allow(missing_docs)]
198 pub _marker: PhantomData<T>,
199 }
200
201 #[pallet::genesis_build]
202 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
203 fn build(&self) {
204 MainChainScriptsConfiguration::<T>::set(self.main_chain_scripts.clone());
205 }
206 }
207
208 #[pallet::inherent]
209 impl<T: Config> ProvideInherent for Pallet<T> {
210 type Call = Call<T>;
211 type Error = InherentError;
212 const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
213
214 fn create_inherent(data: &InherentData) -> Option<Self::Call> {
215 Self::get_transfered_tokens_from_inherent_data(data)
216 .filter(|data| data.token_amount.0 > 0)
217 .map(|data| Call::transfer_tokens { token_amount: data.token_amount })
218 }
219
220 fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
221 let actual_transfer = match call {
222 Call::transfer_tokens { token_amount } => *token_amount,
223 _ => return Ok(()),
224 };
225
226 let expected_transfer = match Self::get_transfered_tokens_from_inherent_data(data) {
227 Some(data) => data.token_amount,
228 None => {
229 return Err(InherentError::UnexpectedTokenTransferInherent(actual_transfer));
230 },
231 };
232
233 if expected_transfer != actual_transfer {
234 return Err(InherentError::IncorrectTokenNumberTransfered(
235 expected_transfer,
236 actual_transfer,
237 ));
238 }
239
240 Ok(())
241 }
242
243 fn is_inherent(call: &Self::Call) -> bool {
244 matches!(call, Call::transfer_tokens { .. })
245 }
246
247 fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
248 Ok(Self::get_transfered_tokens_from_inherent_data(data)
249 .filter(|data| data.token_amount.0 > 0)
250 .map(|data| InherentError::TokenTransferNotHandled(data.token_amount)))
251 }
252 }
253
254 impl<T: Config> Pallet<T> {
255 fn get_transfered_tokens_from_inherent_data(
256 data: &InherentData,
257 ) -> Option<TokenTransferData> {
258 data.get_data::<TokenTransferData>(&INHERENT_IDENTIFIER)
259 .expect("Token transfer data is not encoded correctly")
260 }
261 }
262
263 #[pallet::call]
264 impl<T: Config> Pallet<T> {
265 /// Inherent that registers new native token transfer from the Cardano main chain and triggers
266 /// the handler configured in [Config::TokenTransferHandler].
267 ///
268 /// Arguments:
269 /// - `token_amount`: the total amount of tokens transferred since the last invocation of the inherent
270 #[pallet::call_index(0)]
271 #[pallet::weight((T::WeightInfo::transfer_tokens(), DispatchClass::Mandatory))]
272 pub fn transfer_tokens(
273 origin: OriginFor<T>,
274 token_amount: NativeTokenAmount,
275 ) -> DispatchResult {
276 ensure_none(origin)?;
277 ensure!(
278 MainChainScriptsConfiguration::<T>::exists(),
279 Error::<T>::CalledWithoutConfiguration
280 );
281 ensure!(!TransferedThisBlock::<T>::exists(), Error::<T>::TransferAlreadyHandled);
282 Initialized::<T>::mutate(|initialized| {
283 if !*initialized {
284 *initialized = true
285 }
286 true
287 });
288 TransferedThisBlock::<T>::put(token_amount);
289 Self::deposit_event(Event::TokensTransfered(token_amount));
290 T::TokenTransferHandler::handle_token_transfer(token_amount)
291 }
292
293 /// Changes the main chain scripts used for observing native token transfers.
294 ///
295 /// This extrinsic must be run either using `sudo` or some other chain governance mechanism.
296 #[pallet::call_index(1)]
297 #[pallet::weight((T::WeightInfo::set_main_chain_scripts(), DispatchClass::Normal))]
298 pub fn set_main_chain_scripts(
299 origin: OriginFor<T>,
300 native_token_policy_id: PolicyId,
301 native_token_asset_name: AssetName,
302 illiquid_supply_validator_address: MainchainAddress,
303 ) -> DispatchResult {
304 T::MainChainScriptsOrigin::ensure_origin(origin)?;
305 let new_scripts = sp_native_token_management::MainChainScripts {
306 native_token_policy_id,
307 native_token_asset_name,
308 illiquid_supply_validator_address,
309 };
310 MainChainScriptsConfiguration::<T>::put(new_scripts);
311 Ok(())
312 }
313 }
314
315 #[pallet::hooks]
316 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
317 /// A dummy `on_initialize` to return the amount of weight that `on_finalize` requires to
318 /// execute.
319 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
320 T::WeightInfo::on_finalize()
321 }
322
323 fn on_finalize(_block: BlockNumberFor<T>) {
324 TransferedThisBlock::<T>::kill();
325 }
326 }
327
328 impl<T: Config> Pallet<T> {
329 /// Returns the main chain scripts currently configured in the pallet
330 pub fn get_main_chain_scripts() -> Option<sp_native_token_management::MainChainScripts> {
331 MainChainScriptsConfiguration::<T>::get()
332 }
333 /// Returns the current initialization status of the pallet
334 pub fn initialized() -> bool {
335 Initialized::<T>::get()
336 }
337 }
338}