Skip to main content

Understanding substrate pallet instance

It is not uncommon to run multiple instances of a pallet at the same time on a runtime. This is demonstrated with the collective pallet that has multiple instances running on the Polkadot relay chain. Multiple instances make it possible to reuse a pallet without re-implementing it for a runtime. This allows the implementation of many use cases including having multiple tokens for a liquidity pool pallet with separate supply and distribution.

Substrate enables runtime developers a soft landing, providing valuable traits to implement an instantiable pallet. Substrate also provides smooth handling of unique storage for different instances of a pallet.

In this guide, we will learn how to make a pallet instantiable and highlight how we can test and implement an instantiable pallet on a substrate runtime.

Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!

Reproducing setup

Project setup

To follow along, ensure you have the Rust toolchain installed.

git clone https://github.com/cenwadike/double-auction-node
  • Navigate into the project’s directory.
cd double-auction-node
  • Run the command below to build the pallet.
cargo build --release

Getting some context

The setup above is a substrate node that implements double-auction for electrical energy.

The seller gets matched (and their electricity is sold to the highest bidder) when the auction period is over.

Auctions to be executed are stored in AuctionsExecutionQueue. When an auction is over, it is taken from the AuctionsExecutionQueue and matched to the highest bidder.

Use the setup above as reference to follow along if you get stuck.

Making Pallet instantiable

Unlike regular pallets that have a single instance on a runtime, instantiable pallets must provide clues to the runtime. This clue is provided by adding an instantiable generic I.

The generic I is used to provide a lifetime for a pallet’s generic types and its configuration T T.

We can add the generic type I to the Pallet struct like so:

 #[pallet::pallet]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);

This defines an empty Pallet struct on which we can define a pallet configuration and implement extrinsics.

Making Pallet Config instantiable

To implement a Config trait compatible with our instantiable Pallet, we can need to add the generic type I like so:

  #[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config { // <-- notice I: `static = ()
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;

// ---------------------snip---------------------------
}

This approach defines a 'static lifetime on the generic type I.

Making Storage items instantiable

Because storage items use types defined by the Config trait, we also need to add the generic type I to the storage definition.

Using the AuctionsExecutionQueue we can use the types defined in the Config trait like so:

 #[pallet::storage]
#[pallet::getter(fn auction_end_time)]
pub(super) type AuctionsExecutionQueue<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
_,
Twox64Concat,
BlockNumberFor<T>,
Blake2_128Concat,
T::AuctionId,
(),
OptionQuery,
>;

Notice that we applied the 'static lifetime to the AuctionsExecutionQueue type definition.

Events and Errors for instantiable pallet

Similar to storage items, errors, and events are unique to a Pallet. Events can be defined for an instantiable Pallet like so:

  #[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
AuctionCreated {
auction_id: T::AuctionId,
seller_id: T::AccountId,
energy_quantity: u128,
starting_price: u128,
},

// ------------snip--------------
}

In similar vein Errors can be defined like so:

#[pallet::error]
pub enum Error<T, I = ()> {
AuctionDoesNotExist,

AuctionIsOver,

InsuffficientAttachedDeposit,
}

Notice that no lifetime is required for Error.

Genesis config for instantiable pallet

Unlike the genesis configuration for a previous guide, the genesis configuration using types from the Config trait must the generic type I with a static lifetime like so:

 #[pallet::genesis_config]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub auction_index: T::AuctionId,
}

impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
fn default() -> Self {
Self { auction_index: Default::default() }
}
}

#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
let initial_id = self.auction_index;
<AuctionIndex<T, I>>::put(initial_id);
}
}

Notice how differently I is used on GenesisConfig struct like so GenesisConfig<T: Config<I>, I: 'static = ()> vs on the impl like so impl<T: Config<I>, I: 'static>.

Adding instantiable pallet to runtime

The code snippet below demonstrates how we can implement multiple instances of the pallet_balances.

// -------------snip--------------------

type MainToken = pallet_balances::Instance1;
impl pallet_balances::Config<MainToken> for Runtime {
type MaxLocks = frame_support::traits::ConstU32<1024>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = u128;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = Self::ExistentialDeposit;
type AccountStore = System;
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
}

type DerivativeToken = pallet_balances::Instance2;
impl pallet_balances::Config<DerivativeToken> for Runtime {
type MaxLocks = frame_support::traits::ConstU32<1024>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = u128;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = Self::ExistentialDeposit;
type AccountStore = System;
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
}

// -------------snip--------------------

In comparison a single instance of pallet_balances like so:

// -------------snip--------------------

impl pallet_balances::Config for Runtime {
type MaxLocks = ConstU32<50>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
/// The type for recording an account's balance.
type Balance = Balance;
/// The ubiquitous event type.
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU128<EXISTENTIAL_DEPOSIT>;
type AccountStore = System;
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
type FreezeIdentifier = ();
type MaxFreezes = ();
type RuntimeHoldReason = ();
type MaxHolds = ();
}

// -------------snip--------------------

Common pitfalls when using instantiable pallets

Because blockchain uses unique prefixes to keep track of different storage, care must be taken to prevent two or more instances writing to the same storage item or the storage trie.

A way to avoid this issue is to assign a unique prefix to each instance of a Pallet as shown with pallet_balances here

Summary

We transformed a regular pallet into an instantiable one by adding the generic type I on different pallet components. We gained an understanding of how instantiable pallets are handled by the substrate runtime and used this understanding to highlight error-prone coupling for instantiable pallets.

We developed an understanding of:

  • what the I generic type is and how to use it on a pallet.
  • how to couple an instantiable pallet on a substrate runtime.
  • how substrate runtime handles multiple instances of a pallet.
  • common pitfalls when using instantiable pallets.

To learn more about instantiable pallets, check out these resources:

Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!

grillchat icon