pallet_partner_chains_bridge/
lib.rs

1//! Pallet that tracks information about incoming token bridge transfers observed on Cardano.
2//!
3//! # Purpose of this pallet
4//!
5//! This pallet implements runtime logic necessary for Partner Chains to receive
6//! token transfers from Cardano using the trustless token bridge. It exposes a
7//! callback API for chain builders to hook their own transfer handling logic into
8//! the pallet according to their business and ledger rules.
9//!
10//! # Working overview
11//!
12//! Bridge transfers are initiated by transactions on Cardano that create UTXOs
13//! on the illiquid circulating supply (ICP) validator address, each containing
14//! a datum which marks them as transfer UTXOs. The observability layer of a
15//! Partner Chain node registers creation of these UTXOs and classifies them
16//! either as *user transfers*, ie. transfers sent by normal chain users to a
17//! Partner Chain address specified by the user; or special *reserve transfers*,
18//! which are a mechanism for a Partner Chain to gradually move their token
19//! reserve from Cardano to its own ledger.
20//!
21//! Newly observed and classified bridge transfers are provided to the runtime
22//! as inherent data. Based on this data, the pallet creates an inherent
23//! extrinsic to handle them in the runtime during block production. This
24//! inherent does not process the transfers directly, and instead calls the
25//! handler provided by the particular Partner Chain's builders. This allows
26//! the pallet to not assume anything about the ledger structure and logic of
27//! the Partner Chain.
28//!
29//! # Usage
30//!
31//! ## Define the recipient type
32//!
33//! All user transfers handler by the pallet are addressed to a recipient
34//! specified in the datum of the transfer UTXO. This recipient can be any
35//! type that can be encoded and decoded as a Plutus byte string. A natural
36//! choice would be the account address used in the Partner Chain runtime,
37//! but a different type can be chosen as needed.
38//!
39//! ## Implement the transfer handler
40//!
41//! Because the Substrate framework leaves the developers a lot of freedom in
42//! structuring their ledger and defining runtime logic, the pallet does not
43//! handle the transfers by itself. Instead, it must be configured with a
44//! [TransferHandler] instance by the Partner Chain builder.
45//!
46//! This handler is expected to never fail and handle any errors internally,
47//! unless there exists a case in which the chain should very deliberately
48//! be unable to produce a block. In practice, this means that any invalid
49//! transfers should be either discarded or saved for reprocessing later.
50//!
51//! A minimal example for a runtime that uses `pallet_balances` and `AccountId32`
52//! as its recipient type could look like this:
53//!
54//! ```rust,ignore
55//! pub struct BridgeTransferHelper;
56//!
57//! impl pallet_partner_chains_bridge::TransferHandler<AccountId32> for BridgeTransferHelper {
58//! 	fn handle_incoming_transfer(transfer: BridgeTransferV1<AccountId32>) {
59//! 		match transfer {
60//! 			BridgeTransferV1::InvalidTransfer { token_amount, utxo_id } => {
61//! 				log::warn!("⚠️ Discarded an invalid transfer of {token_amount} (utxo {utxo_id})");
62//! 			},
63//! 			BridgeTransferV1::UserTransfer { token_amount, recipient } => {
64//! 				log::info!("💸 Registered a tranfer of {token_amount} to {recipient:?}");
65//! 				let _ = Balances::deposit_creating(&recipient, token_amount.into());
66//! 			},
67//! 			BridgeTransferV1::ReserveTransfer { token_amount } => {
68//! 				log::info!("🏦 Registered a reserve transfer of {token_amount}.");
69//! 				let _ = Balances::deposit_creating(&T::ReserveAccount::get(), token_amount.into());
70//! 			},
71//! 		}
72//! 	}
73//! }
74//! ```rust
75//!
76//! For runtimes that require more complex transfer handling logic, it is a good
77//! practice to create a dedicated pallet in the runtime and have it implement
78//! [TransferHandler], so that any relevant state and configuration can be stored
79//! together.
80//!
81//! ## Adding the pallet to the runtime
82//!
83//! Add the pallet to your runtime's [construct_runtime] and configure it by supplying
84//! all relevant types from your runtime:
85//!
86//! ```rust,ignore
87//! parameter_types! {
88//!     pub const MaxTransfersPerBlock: u32 = 256;
89//! }
90//!
91//! impl pallet_partner_chains_bridge::Config for Runtime {
92//! 	type GovernanceOrigin = EnsureRoot<Runtime>;
93//! 	type Recipient = AccountId;
94//! 	type TransferHandler = BridgeTransferHelper;
95//! 	type MaxTransfersPerBlock = MaxTransfersPerBlock;
96//! 	type WeightInfo = ();
97//!
98//! 	#[cfg(feature = "runtime-benchmarks")]
99//! 	type BenchmarkHelper = ();
100//! }
101//! ```
102//!
103//! In particular, the pallet needs to be configured with the value  `MaxTransfersPerBlock`,
104//! which determines the upper bound on the number of transactions that can be processed
105//! per block. All outstanding transfers beyond that limit will be processed in subsequent
106//! block. It is important to select a value high enough to guarantee that the chain will
107//! be able to keep up with the volume of transfers coming in.
108//!
109//! The last thing to implement in the runtime is the runtime API used by the observability
110//! layer to access the configuration stored in the pallet. This is straightforward and
111//! involves only calling methods defined on the pallet:
112//!
113//! ```rust,ignore
114//! impl sp_partner_chains_bridge::TokenBridgeIDPRuntimeApi<Block> for Runtime {
115//! 	fn get_pallet_version() -> u32 {
116//! 		Bridge::get_pallet_version()
117//! 	}
118//! 	fn get_main_chain_scripts() -> Option<BridgeMainChainScripts> {
119//! 		Bridge::get_main_chain_scripts()
120//! 	}
121//! 	fn get_max_transfers_per_block() -> u32 {
122//! 		Bridge::get_max_transfers_per_block()
123//! 	}
124//! 	fn get_last_data_checkpoint() -> Option<BridgeDataCheckpoint> {
125//! 		Bridge::get_data_checkpoint()
126//! 	}
127//! }
128//! ```
129//!
130//! ## Providing genesis configuration
131//!
132//! The pallet's genesis configuration only consists of optional values of
133//! the main chain scripts, that can be set after chain start. These scripts
134//! point the observability layer to the correct addresses and token asset
135//! to observe on Cardano. If they are left empty, the pallet and the
136//! observability components will be incactive until they are supplied in
137//! the future.
138//!
139//! An example of a bridge pallet section in a genesis config JSON would
140//! look like this:
141//! ```json
142//! {
143//!     "bridge": {
144//!       "mainChainScripts": {
145//!         "illiquid_circulation_supply_validator_address": "addr_test1wzzyc3mcqh4phq0pa827dn756lfd045lzh3tgr9mt5p2ayqpxp55c",
146//!         "token_asset_name": "0x5043546f6b656e44656d6f",
147//!         "token_policy_id": "0xada83ddd029614381f00e28de0922ab0dec6983ea9dd29ae20eef9b4"
148//!       }
149//! 	}
150//! }
151//! ```
152//!
153//! When programmatically assembling the genesis config, a utility function
154//! is supplied for reading the main chain script values from environment:
155//!
156//! ```rust
157//! # use pallet_partner_chains_bridge::{ Config, GenesisConfig };
158//! # use sp_partner_chains_bridge::MainChainScripts;
159//! # fn create_genesis_config<T: Config>() -> GenesisConfig<T> {
160//! 	GenesisConfig {
161//! 		main_chain_scripts: MainChainScripts::read_from_env().ok(),
162//! 		..Default::default()
163//! 	}
164//! # }
165//! ```
166//!
167//! See [sp_partner_chains_bridge::MainChainScripts::read_from_env] for details.
168//!
169//! ## Supplying observability data
170//!
171//! See documentation of [sp_partner_chains_bridge] for instructions on adding
172//! the observability data source to your node and connecting it to the pallet.
173//!
174#![cfg_attr(not(feature = "std"), no_std)]
175#![deny(missing_docs)]
176
177extern crate alloc;
178
179#[cfg(test)]
180mod tests;
181
182#[cfg(test)]
183mod mock;
184
185/// Pallet benchmarking code
186#[cfg(feature = "runtime-benchmarks")]
187pub mod benchmarking;
188
189/// Weight types and default weight values
190pub mod weights;
191
192pub use pallet::*;
193use sp_partner_chains_bridge::BridgeTransferV1;
194
195/// Runtime logic for handling incoming token bridge transfers from Cardano
196///
197/// The chain builder should implement in accordance with their particular business rules and
198/// ledger structure. Calls to all functions defined by this trait should not return any errors
199/// as this would fail the block creation. Instead, any validation and business logic errors
200/// should be handled gracefully inside the handler code.
201pub trait TransferHandler<Recipient> {
202	/// Should handle an incoming token transfer of `token_mount` tokens to `recipient`
203	fn handle_incoming_transfer(_transfer: BridgeTransferV1<Recipient>);
204}
205
206/// No-op implementation of `TransferHandler` for unit type.
207impl<Recipient> TransferHandler<Recipient> for () {
208	fn handle_incoming_transfer(_transfer: BridgeTransferV1<Recipient>) {}
209}
210
211#[frame_support::pallet]
212pub mod pallet {
213	use super::*;
214	use crate::weights::WeightInfo;
215	use frame_support::pallet_prelude::*;
216	use frame_system::{ensure_none, pallet_prelude::OriginFor};
217	use parity_scale_codec::MaxEncodedLen;
218	use sidechain_domain::UtxoId;
219	use sp_partner_chains_bridge::{
220		BridgeDataCheckpoint, INHERENT_IDENTIFIER, InherentError, MainChainScripts,
221		TokenBridgeTransfersV1,
222	};
223
224	/// Current version of the pallet
225	pub const PALLET_VERSION: u32 = 1;
226
227	#[pallet::pallet]
228	pub struct Pallet<T>(_);
229
230	#[pallet::config]
231	pub trait Config: frame_system::Config {
232		/// Origin for governance extrinsic calls.
233		///
234		/// Typically the `EnsureRoot` type can be used unless a non-standard on-chain governance is used.
235		type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
236
237		/// Transfer recipient
238		type Recipient: Member + Parameter + MaxEncodedLen;
239
240		/// Handler for incoming token transfers
241		type TransferHandler: TransferHandler<Self::Recipient>;
242
243		/// Maximum number of transfers that can be handled in one block for each transfer type
244		type MaxTransfersPerBlock: Get<u32>;
245
246		/// Extrinsic weight information
247		type WeightInfo: crate::weights::WeightInfo;
248
249		/// Benchmark helper type used for running benchmarks
250		#[cfg(feature = "runtime-benchmarks")]
251		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self>;
252	}
253
254	/// Error type used by the pallet's extrinsics
255	#[pallet::error]
256	pub enum Error<T> {}
257
258	#[pallet::storage]
259	pub type MainChainScriptsConfiguration<T: Config> =
260		StorageValue<_, MainChainScripts, OptionQuery>;
261
262	#[pallet::storage]
263	pub type DataCheckpoint<T: Config> = StorageValue<_, BridgeDataCheckpoint, OptionQuery>;
264
265	/// Genesis configuration of the pallet
266	#[pallet::genesis_config]
267	pub struct GenesisConfig<T: Config> {
268		/// Initial main chain scripts
269		pub main_chain_scripts: Option<MainChainScripts>,
270		/// The initial data checkpoint. Chain Genesis UTXO is a good candidate for it.
271		pub initial_checkpoint: Option<UtxoId>,
272		#[allow(missing_docs)]
273		pub _marker: PhantomData<T>,
274	}
275
276	impl<T: Config> Default for GenesisConfig<T> {
277		fn default() -> Self {
278			Self { main_chain_scripts: None, initial_checkpoint: None, _marker: Default::default() }
279		}
280	}
281
282	#[pallet::genesis_build]
283	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
284		fn build(&self) {
285			MainChainScriptsConfiguration::<T>::set(self.main_chain_scripts.clone());
286			DataCheckpoint::<T>::set(self.initial_checkpoint.map(BridgeDataCheckpoint::Utxo));
287		}
288	}
289
290	#[pallet::call]
291	impl<T: Config> Pallet<T> {
292		/// Inherent extrinsic that handles all incoming transfers in the current block
293		#[pallet::call_index(0)]
294		#[pallet::weight((T::WeightInfo::handle_transfers(transfers.len() as u32), DispatchClass::Mandatory))]
295		pub fn handle_transfers(
296			origin: OriginFor<T>,
297			transfers: BoundedVec<BridgeTransferV1<T::Recipient>, T::MaxTransfersPerBlock>,
298			data_checkpoint: BridgeDataCheckpoint,
299		) -> DispatchResult {
300			ensure_none(origin)?;
301			for transfer in transfers {
302				T::TransferHandler::handle_incoming_transfer(transfer);
303			}
304			DataCheckpoint::<T>::put(data_checkpoint);
305			Ok(())
306		}
307
308		/// Changes the main chain scripts used for observing native token transfers along with a new data checkpoint.
309		///
310		/// This extrinsic must be run either using `sudo` or some other chain governance mechanism.
311		///
312		///
313		#[pallet::call_index(1)]
314		#[pallet::weight(T::WeightInfo::set_main_chain_scripts())]
315		pub fn set_main_chain_scripts(
316			origin: OriginFor<T>,
317			new_scripts: MainChainScripts,
318			data_checkpoint: BridgeDataCheckpoint,
319		) -> DispatchResult {
320			T::GovernanceOrigin::ensure_origin(origin)?;
321			MainChainScriptsConfiguration::<T>::put(new_scripts);
322			DataCheckpoint::<T>::put(data_checkpoint);
323			Ok(())
324		}
325	}
326
327	#[pallet::inherent]
328	impl<T: Config> ProvideInherent for Pallet<T> {
329		type Call = Call<T>;
330		type Error = InherentError;
331		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
332
333		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
334			let data = Self::decode_inherent_data(data)?;
335			let transfers = data.transfers.try_into().expect(
336				"The number of transfers in the inherent data must be within configured bounds",
337			);
338			Some(Call::handle_transfers { transfers, data_checkpoint: data.data_checkpoint })
339		}
340
341		fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
342			let Some(expected_call) = Self::create_inherent(data) else {
343				return Err(Self::Error::InherentNotExpected);
344			};
345
346			if *call != expected_call {
347				return Err(Self::Error::IncorrectInherent);
348			}
349
350			Ok(())
351		}
352
353		fn is_inherent(call: &Self::Call) -> bool {
354			matches!(call, Call::handle_transfers { .. })
355		}
356
357		fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
358			match Self::decode_inherent_data(data) {
359				None => Ok(None),
360				Some(_) => Ok(Some(Self::Error::InherentRequired)),
361			}
362		}
363	}
364
365	impl<T: Config> Pallet<T> {
366		fn decode_inherent_data(
367			data: &InherentData,
368		) -> Option<TokenBridgeTransfersV1<T::Recipient>> {
369			data.get_data(&INHERENT_IDENTIFIER)
370				.expect("Bridge inherent data is not encoded correctly")
371		}
372	}
373
374	impl<T: Config> Pallet<T> {
375		/// Returns current pallet version
376		pub fn get_pallet_version() -> u32 {
377			PALLET_VERSION
378		}
379
380		/// Returns the currently configured main chain scripts
381		pub fn get_main_chain_scripts() -> Option<MainChainScripts> {
382			MainChainScriptsConfiguration::<T>::get()
383		}
384
385		/// Returns the currently configured transfers per block limit
386		pub fn get_max_transfers_per_block() -> u32 {
387			T::MaxTransfersPerBlock::get()
388		}
389
390		/// Returns the current data checkpoint
391		pub fn get_data_checkpoint() -> Option<BridgeDataCheckpoint> {
392			DataCheckpoint::<T>::get()
393		}
394	}
395}