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 #[pallet::constant]
245 type MaxTransfersPerBlock: Get<u32>;
246
247 /// Extrinsic weight information
248 type WeightInfo: crate::weights::WeightInfo;
249
250 /// Benchmark helper type used for running benchmarks
251 #[cfg(feature = "runtime-benchmarks")]
252 type BenchmarkHelper: benchmarking::BenchmarkHelper<Self>;
253 }
254
255 /// Error type used by the pallet's extrinsics
256 #[pallet::error]
257 pub enum Error<T> {}
258
259 #[pallet::storage]
260 pub type MainChainScriptsConfiguration<T: Config> =
261 StorageValue<_, MainChainScripts, OptionQuery>;
262
263 #[pallet::storage]
264 pub type DataCheckpoint<T: Config> = StorageValue<_, BridgeDataCheckpoint, OptionQuery>;
265
266 /// Genesis configuration of the pallet
267 #[pallet::genesis_config]
268 pub struct GenesisConfig<T: Config> {
269 /// Initial main chain scripts
270 pub main_chain_scripts: Option<MainChainScripts>,
271 /// The initial data checkpoint. Chain Genesis UTXO is a good candidate for it.
272 pub initial_checkpoint: Option<UtxoId>,
273 #[allow(missing_docs)]
274 pub _marker: PhantomData<T>,
275 }
276
277 impl<T: Config> Default for GenesisConfig<T> {
278 fn default() -> Self {
279 Self { main_chain_scripts: None, initial_checkpoint: None, _marker: Default::default() }
280 }
281 }
282
283 #[pallet::genesis_build]
284 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
285 fn build(&self) {
286 MainChainScriptsConfiguration::<T>::set(self.main_chain_scripts.clone());
287 DataCheckpoint::<T>::set(self.initial_checkpoint.map(BridgeDataCheckpoint::Utxo));
288 }
289 }
290
291 #[pallet::call]
292 impl<T: Config> Pallet<T> {
293 /// Inherent extrinsic that handles all incoming transfers in the current block
294 #[pallet::call_index(0)]
295 #[pallet::weight((T::WeightInfo::handle_transfers(transfers.len() as u32), DispatchClass::Mandatory))]
296 pub fn handle_transfers(
297 origin: OriginFor<T>,
298 transfers: BoundedVec<BridgeTransferV1<T::Recipient>, T::MaxTransfersPerBlock>,
299 data_checkpoint: BridgeDataCheckpoint,
300 ) -> DispatchResult {
301 ensure_none(origin)?;
302 for transfer in transfers {
303 T::TransferHandler::handle_incoming_transfer(transfer);
304 }
305 DataCheckpoint::<T>::put(data_checkpoint);
306 Ok(())
307 }
308
309 /// Changes the main chain scripts used for observing native token transfers along with a new data checkpoint.
310 ///
311 /// This extrinsic must be run either using `sudo` or some other chain governance mechanism.
312 ///
313 ///
314 #[pallet::call_index(1)]
315 #[pallet::weight(T::WeightInfo::set_main_chain_scripts())]
316 pub fn set_main_chain_scripts(
317 origin: OriginFor<T>,
318 new_scripts: MainChainScripts,
319 data_checkpoint: BridgeDataCheckpoint,
320 ) -> DispatchResult {
321 T::GovernanceOrigin::ensure_origin(origin)?;
322 MainChainScriptsConfiguration::<T>::put(new_scripts);
323 DataCheckpoint::<T>::put(data_checkpoint);
324 Ok(())
325 }
326 }
327
328 #[pallet::inherent]
329 impl<T: Config> ProvideInherent for Pallet<T> {
330 type Call = Call<T>;
331 type Error = InherentError;
332 const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
333
334 fn create_inherent(data: &InherentData) -> Option<Self::Call> {
335 let data = Self::decode_inherent_data(data)?;
336 let transfers = data.transfers.try_into().expect(
337 "The number of transfers in the inherent data must be within configured bounds",
338 );
339 Some(Call::handle_transfers { transfers, data_checkpoint: data.data_checkpoint })
340 }
341
342 fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
343 let Some(expected_call) = Self::create_inherent(data) else {
344 return Err(Self::Error::InherentNotExpected);
345 };
346
347 if *call != expected_call {
348 return Err(Self::Error::IncorrectInherent);
349 }
350
351 Ok(())
352 }
353
354 fn is_inherent(call: &Self::Call) -> bool {
355 matches!(call, Call::handle_transfers { .. })
356 }
357
358 fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
359 match Self::decode_inherent_data(data) {
360 None => Ok(None),
361 Some(_) => Ok(Some(Self::Error::InherentRequired)),
362 }
363 }
364 }
365
366 impl<T: Config> Pallet<T> {
367 fn decode_inherent_data(
368 data: &InherentData,
369 ) -> Option<TokenBridgeTransfersV1<T::Recipient>> {
370 data.get_data(&INHERENT_IDENTIFIER)
371 .expect("Bridge inherent data is not encoded correctly")
372 }
373 }
374
375 impl<T: Config> Pallet<T> {
376 /// Returns current pallet version
377 pub fn get_pallet_version() -> u32 {
378 PALLET_VERSION
379 }
380
381 /// Returns the currently configured main chain scripts
382 pub fn get_main_chain_scripts() -> Option<MainChainScripts> {
383 MainChainScriptsConfiguration::<T>::get()
384 }
385
386 /// Returns the currently configured transfers per block limit
387 pub fn get_max_transfers_per_block() -> u32 {
388 T::MaxTransfersPerBlock::get()
389 }
390
391 /// Returns the current data checkpoint
392 pub fn get_data_checkpoint() -> Option<BridgeDataCheckpoint> {
393 DataCheckpoint::<T>::get()
394 }
395 }
396}