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}