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}