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 [crate::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 [Pallet::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 [Pallet::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		/// Origin for governance calls
144		type MainChainScriptsOrigin: EnsureOrigin<Self::RuntimeOrigin>;
145
146		/// Event handler for incoming native token transfers
147		type TokenTransferHandler: TokenTransferHandler;
148
149		/// Weight information for this pallet's extrinsics
150		type WeightInfo: WeightInfo;
151	}
152
153	/// Events emitted by this pallet
154	#[pallet::event]
155	#[pallet::generate_deposit(pub(super) fn deposit_event)]
156	pub enum Event<T: Config> {
157		/// Signals that a new native token transfer has been processed by the pallet
158		TokensTransfered(NativeTokenAmount),
159	}
160
161	/// Error type used by the pallet's extrinsics
162	#[pallet::error]
163	pub enum Error<T> {
164		/// Indicates that the inherent was called while there was no main chain scripts set in the
165		/// pallet's storage. This is indicative of a programming bug.
166		CalledWithoutConfiguration,
167		/// Indicates that the inherent was called a second time in the same block
168		TransferAlreadyHandled,
169	}
170
171	#[pallet::storage]
172	pub type MainChainScriptsConfiguration<T: Config> =
173		StorageValue<_, sp_native_token_management::MainChainScripts, OptionQuery>;
174
175	/// Stores the pallet's initialization state.
176	///
177	/// The pallet is considered initialized if its inherent has been successfuly called at least once since
178	/// genesis or the last invocation of [Pallet::set_main_chain_scripts].
179	#[pallet::storage]
180	pub type Initialized<T: Config> = StorageValue<_, bool, ValueQuery>;
181
182	/// Transient storage containing the amount of native token transfer registered in the current block.
183	///
184	/// Any value in this storage is only present during execution of a block and is emptied on block finalization.
185	#[pallet::storage]
186	pub type TransferedThisBlock<T: Config> = StorageValue<_, NativeTokenAmount, OptionQuery>;
187
188	/// Genesis configuration of the pallet
189	#[pallet::genesis_config]
190	#[derive(frame_support::DefaultNoBound)]
191	pub struct GenesisConfig<T: Config> {
192		/// Initial main chain scripts
193		pub main_chain_scripts: Option<sp_native_token_management::MainChainScripts>,
194		#[allow(missing_docs)]
195		pub _marker: PhantomData<T>,
196	}
197
198	#[pallet::genesis_build]
199	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
200		fn build(&self) {
201			MainChainScriptsConfiguration::<T>::set(self.main_chain_scripts.clone());
202		}
203	}
204
205	#[pallet::inherent]
206	impl<T: Config> ProvideInherent for Pallet<T> {
207		type Call = Call<T>;
208		type Error = InherentError;
209		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
210
211		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
212			Self::get_transfered_tokens_from_inherent_data(data)
213				.filter(|data| data.token_amount.0 > 0)
214				.map(|data| Call::transfer_tokens { token_amount: data.token_amount })
215		}
216
217		fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
218			let actual_transfer = match call {
219				Call::transfer_tokens { token_amount } => *token_amount,
220				_ => return Ok(()),
221			};
222
223			let expected_transfer = match Self::get_transfered_tokens_from_inherent_data(data) {
224				Some(data) => data.token_amount,
225				None => {
226					return Err(InherentError::UnexpectedTokenTransferInherent(actual_transfer));
227				},
228			};
229
230			if expected_transfer != actual_transfer {
231				return Err(InherentError::IncorrectTokenNumberTransfered(
232					expected_transfer,
233					actual_transfer,
234				));
235			}
236
237			Ok(())
238		}
239
240		fn is_inherent(call: &Self::Call) -> bool {
241			matches!(call, Call::transfer_tokens { .. })
242		}
243
244		fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
245			Ok(Self::get_transfered_tokens_from_inherent_data(data)
246				.filter(|data| data.token_amount.0 > 0)
247				.map(|data| InherentError::TokenTransferNotHandled(data.token_amount)))
248		}
249	}
250
251	impl<T: Config> Pallet<T> {
252		fn get_transfered_tokens_from_inherent_data(
253			data: &InherentData,
254		) -> Option<TokenTransferData> {
255			data.get_data::<TokenTransferData>(&INHERENT_IDENTIFIER)
256				.expect("Token transfer data is not encoded correctly")
257		}
258	}
259
260	#[pallet::call]
261	impl<T: Config> Pallet<T> {
262		/// Inherent that registers new native token transfer from the Cardano main chain and triggers
263		/// the handler configured in [Config::TokenTransferHandler].
264		///
265		/// Arguments:
266		/// - `token_amount`: the total amount of tokens transferred since the last invocation of the inherent
267		#[pallet::call_index(0)]
268		#[pallet::weight((T::WeightInfo::transfer_tokens(), DispatchClass::Mandatory))]
269		pub fn transfer_tokens(
270			origin: OriginFor<T>,
271			token_amount: NativeTokenAmount,
272		) -> DispatchResult {
273			ensure_none(origin)?;
274			ensure!(
275				MainChainScriptsConfiguration::<T>::exists(),
276				Error::<T>::CalledWithoutConfiguration
277			);
278			ensure!(!TransferedThisBlock::<T>::exists(), Error::<T>::TransferAlreadyHandled);
279			Initialized::<T>::mutate(|initialized| {
280				if !*initialized {
281					*initialized = true
282				}
283				true
284			});
285			TransferedThisBlock::<T>::put(token_amount);
286			Self::deposit_event(Event::TokensTransfered(token_amount));
287			T::TokenTransferHandler::handle_token_transfer(token_amount)
288		}
289
290		/// Changes the main chain scripts used for observing native token transfers.
291		///
292		/// This extrinsic must be run either using `sudo` or some other chain governance mechanism.
293		#[pallet::call_index(1)]
294		#[pallet::weight((T::WeightInfo::set_main_chain_scripts(), DispatchClass::Normal))]
295		pub fn set_main_chain_scripts(
296			origin: OriginFor<T>,
297			native_token_policy_id: PolicyId,
298			native_token_asset_name: AssetName,
299			illiquid_supply_validator_address: MainchainAddress,
300		) -> DispatchResult {
301			T::MainChainScriptsOrigin::ensure_origin(origin)?;
302			let new_scripts = sp_native_token_management::MainChainScripts {
303				native_token_policy_id,
304				native_token_asset_name,
305				illiquid_supply_validator_address,
306			};
307			MainChainScriptsConfiguration::<T>::put(new_scripts);
308			Ok(())
309		}
310	}
311
312	#[pallet::hooks]
313	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
314		/// A dummy `on_initialize` to return the amount of weight that `on_finalize` requires to
315		/// execute.
316		fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
317			T::WeightInfo::on_finalize()
318		}
319
320		fn on_finalize(_block: BlockNumberFor<T>) {
321			TransferedThisBlock::<T>::kill();
322		}
323	}
324
325	impl<T: Config> Pallet<T> {
326		/// Returns the main chain scripts currently configured in the pallet
327		pub fn get_main_chain_scripts() -> Option<sp_native_token_management::MainChainScripts> {
328			MainChainScriptsConfiguration::<T>::get()
329		}
330		/// Returns the current initialization status of the pallet
331		pub fn initialized() -> bool {
332			Initialized::<T>::get()
333		}
334	}
335}