referrerpolicy=no-referrer-when-downgrade
Expand description

Learn how Substrate and FRAME use traits and associated types to make modules generic in a type-safe manner.

§Trait-based Programming

This document walks you over a peculiar way of using Rust’s trait items. This pattern is abundantly used within frame and is therefore paramount important for a smooth transition into it.

The rest of this document assumes familiarity with the Rust book’s Advanced Traits section. Moreover, we use the [frame::traits::Get].

First, imagine we are writing a FRAME pallet. We represent this pallet with a struct Pallet, and this pallet wants to implement the functionalities of that pallet, for example a simple transfer function. For the sake of education, we are interested in having a MinTransfer amount, expressed as a [frame::traits::Get], which will dictate what is the minimum amount that can be transferred.

We can foremost write this as simple as the following snippet:

mod basic {
	struct Pallet;

	type AccountId = frame::deps::sp_runtime::AccountId32;
	type Balance = u128;
	type MinTransfer = frame::traits::ConstU128<10>;

	impl Pallet {
		fn transfer(_from: AccountId, _to: AccountId, _amount: Balance) {
			todo!()
		}
	}
}

In this example, we use arbitrary choices for AccountId, Balance and the MinTransfer type. This works great for one team’s purposes but we have to remember that Substrate and FRAME are written as generic frameworks, intended to be highly configurable.

In a broad sense, there are two avenues in exposing configurability:

  1. For values that need to be generic, for example MinTransfer, we attach them to the Pallet struct as fields:
struct Pallet {
	min_transfer: u128,
}
  1. For types that need to be generic, we would have to use generic or associated types, such as:
struct Pallet<AccountId> {
	min_transfer: u128,
    _marker: std::marker::PhantomData<AccountId>,
}

Substrate and FRAME, for various reasons (performance, correctness, type safety) has opted to use types to declare both values and types as generic. This is the essence of why the Get trait exists.

This would bring us to the second iteration of the pallet, which would look like:

mod generic {
	use super::*;

	struct Pallet<AccountId, Balance, MinTransfer> {
		_marker: std::marker::PhantomData<(AccountId, Balance, MinTransfer)>,
	}

	impl<AccountId, Balance, MinTransfer> Pallet<AccountId, Balance, MinTransfer>
	where
		Balance: frame::traits::AtLeast32BitUnsigned,
		MinTransfer: frame::traits::Get<Balance>,
		AccountId: From<[u8; 32]>,
	{
		fn transfer(_from: AccountId, _to: AccountId, amount: Balance) {
			assert!(amount >= MinTransfer::get());
			unimplemented!();
		}
	}
}

In this example, we managed to make all 3 of our types generic. Taking the example of the AccountId, one should read the above as following:

The Pallet does not know what type AccountId concretely is, but it knows that it is something that adheres to being From<[u8; 32]>.

This method would work, but it suffers from two downsides:

  1. It is verbose, each impl block would have to reiterate all of the trait bounds.
  2. It cannot easily share/inherit generic types. Imagine multiple pallets wanting to be generic over a single AccountId. There is no easy way to express that in this model.

Finally, this brings us to using traits and associated types on traits to express the above. Trait associated types have the benefit of:

  1. Being less verbose, as in effect they can group multiple types together.
  2. Can inherit from one another by declaring supertraits.

Interestingly, one downside of associated types is that declaring defaults on them is not stable yet. In the meantime, we have built our own custom mechanics around declaring defaults for associated types, see pallet_default_config_example.

The last iteration of our code would look like this:

mod trait_based {
	use super::*;

	trait Config {
		type AccountId: From<[u8; 32]>;
		type Balance: frame::traits::AtLeast32BitUnsigned;
		type MinTransfer: frame::traits::Get<Self::Balance>;
	}

	struct Pallet<T: Config>(std::marker::PhantomData<T>);
	impl<T: Config> Pallet<T> {
		fn transfer(_from: T::AccountId, _to: T::AccountId, amount: T::Balance) {
			assert!(amount >= T::MinTransfer::get());
			unimplemented!();
		}
	}
}

Notice how instead of having multiple generics, everything is generic over a single <T: Config>, and all types are fetched through T, for example T::AccountId, T::MinTransfer.

Finally, imagine all pallets wanting to be generic over AccountId. This can be achieved by having individual trait Configs declare a shared trait SystemConfig as their supertrait.

mod with_system {
	use super::*;

	pub trait SystemConfig {
		type AccountId: From<[u8; 32]>;
	}

	pub trait Config: SystemConfig {
		type Balance: frame::traits::AtLeast32BitUnsigned;
		type MinTransfer: frame::traits::Get<Self::Balance>;
	}

	pub struct Pallet<T: Config>(std::marker::PhantomData<T>);
	impl<T: Config> Pallet<T> {
		fn transfer(_from: T::AccountId, _to: T::AccountId, amount: T::Balance) {
			assert!(amount >= T::MinTransfer::get());
			unimplemented!();
		}
	}
}

In FRAME, this shared supertrait is frame::prelude::frame_system.

Notice how this made no difference in the syntax of the rest of the code. T::AccountId is still a valid type, since T implements Config and Config implies SystemConfig, which has a type AccountId.

Note, in some instances one would need to use what is known as the fully-qualified-syntax to access a type to help the Rust compiler disambiguate.

mod fully_qualified {
	use super::with_system::*;

	// Example of using fully qualified syntax.
	type AccountIdOf<T> = <T as SystemConfig>::AccountId;
}

This syntax can sometimes become more complicated when you are dealing with nested traits. Consider the following example, in which we fetch the type Balance from another trait CurrencyTrait.

mod fully_qualified_complicated {
	use super::with_system::*;

	trait CurrencyTrait {
		type Balance: frame::traits::AtLeast32BitUnsigned;
		fn more_stuff() {}
	}

	trait Config: SystemConfig {
		type Currency: CurrencyTrait;
	}

	struct Pallet<T: Config>(std::marker::PhantomData<T>);
	impl<T: Config> Pallet<T> {
		fn transfer(
			_from: T::AccountId,
			_to: T::AccountId,
			_amount: <<T as Config>::Currency as CurrencyTrait>::Balance,
		) {
			unimplemented!();
		}
	}

	/// A common pattern in FRAME.
	type BalanceOf<T> = <<T as Config>::Currency as CurrencyTrait>::Balance;
}

Notice the final type BalanceOf and how it is defined. Using such aliases to shorten the length of fully qualified syntax is a common pattern in FRAME.

The above example is almost identical to the well-known (and somewhat notorious) type BalanceOf that is often used in the context of frame::traits::fungible.

pub(crate) type BalanceOf<T> =
	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

§Additional Resources