Implement lockable currency
This guide shows you how to write a pallet that allows users to lock funds for staking and voting.
The LockableCurrency
trait is useful in the context of economic systems that enforce accountability by collateralizing fungible resources.
You can use the Substrate staking pallet to manage locked funds in time-based increments.
In this guide, we will implement the set_lock
, extend_lock
and remove_lock
methods in our own custom pallet.
Before you begin
You will need to have a pallet already integrated in a runtime to follow this guide. This guide assumes you are using a runtime that contains the Balances pallet to handle the accounts and balances for your chain. You can use the template pallet in the node template to follow.
Declare the necessary dependencies
The methods from LockableCurrency
require us to import a few traits from frame_support
.
-
Ensure you have the following traits imported in the top section of your pallet:
use frame_support::{ dispatch::DispatchResult, traits::{Currency, LockIdentifier, LockableCurrency, WithdrawReasons}, };
-
Declare the
LockIdentifier
constant.To use
LockableCurrency
, you must declare aLockIdentifier
. Given that it requires a[u8; 8]
type, this identifier must be eight characters long.const EXAMPLE_ID: LockIdentifier = *b"example ";
You will need this to declare the methods we'll be including later on.
Declare your custom pallet types
Defining your custom configuration type will allow your pallet to inherit the methods we want to implement.
-
Define the lockable currency type, let's call it
StakeCurrency
:type StakeCurrency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
-
Define a type to satisfy the associated type
Balance
from theCurrency
trait we're using:type BalanceOf<T> = <<T as Config>::StakeCurrency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
This ensures that our pallet has a type to handle the
amount
field in the methods we'll be implementing. -
Specify the new type for your runtime:
impl pallet_template::Config for Runtime { type RuntimeEvent = RuntimeEvent; type StakeCurrency = Balances; // <- add this line }
Passing in
Balances
ensures that your pallet'sLockableCurrency
methods have the same understanding ofBalance
than the pallet that handles accounts balances of your blockchain.
Create dispatchable functions using the required methods
The required methods are:
fn lock_capital
: Locks the specified amount of tokens from the caller.fn extend_lock
: Extends the lock period.fn unlock_all
: Releases all locked tokens.-
Write
lock_capital
, usingT::StakeCurrency::set_lock
:#[pallet::weight(10_000 + T::DbWeight::get().writes(1))] pub(super) fn lock_capital( origin: OriginFor<T>, #[pallet::compact] amount: BalanceOf<T> ) -> DispatchResultWithPostInfo { let user = ensure_signed(origin)?; T::StakeCurrency::set_lock( EXAMPLE_ID, &user, amount, WithdrawReasons::all(), ); Self::deposit_event(Event::Locked(user, amount)); Ok(().into()) }
-
Write
extend_lock
, usingT::StakeCurrency::extend_lock
:#[pallet::weight(1_000)] pub(super) fn extend_lock( origin: OriginFor<T>, #[pallet::compact] amount: BalanceOf<T>, ) -> DispatchResultWithPostInfo { let user = ensure_signed(origin)?; T::StakeCurrency::extend_lock( EXAMPLE_ID, &user, amount, WithdrawReasons::all(), ); Self::deposit_event(Event::ExtendedLock(user, amount)); Ok(().into()) }
-
Write
unlock_all
, usingT::StakeCurrency::remove_lock
:#[pallet::weight(1_000)] pub(super) fn unlock_all( origin: OriginFor<T>, ) -> DispatchResultWithPostInfo { let user = ensure_signed(origin)?; T::StakeCurrency::remove_lock(EXAMPLE_ID, &user); Self::deposit_event(Event::Unlocked(user)); Ok(().into()) }
-
Write the events for all three dispatchables:
#[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Locked(T::AccountId, BalanceOf<T>), Unlocked(T::AccountId), LockExtended(T::AccountId, BalanceOf<T>), }
Examples
lockable-currency
example pallet