Basic Token

pallets/basic-token

This recipe demonstrates a simple but functional token in a pallet.

Mapping Accounts to Balances

Mappings are a very powerful primitive. A stateful cryptocurrency might store a mapping between accounts and balances. Likewise, mappings prove useful when representing owned data. By tracking ownership with maps, it is easy manage permissions for modifying values specific to individual users or groups.

Storage Items

The primary storage item is the mapping between AccountIds and Balances described above. Every account that holds tokens appears as a key in that map and its value is the number of tokens it holds.

The next two storage items set the total supply of the token and keep track of whether the token has been initialized yet.

decl_storage! {
	trait Store for Module<T: Trait> as Token {
		pub Balances get(get_balance): map hasher(blake2_128_concat) T::AccountId => u64;

		pub TotalSupply get(total_supply): u64 = 21000000;

		Init get(is_init): bool;
	}
}

Because users can influence the keys in our storage map, we've chosen the blake2_128_concat hasher as described in the recipe on storage mapss.

Events and Errors

The pallet defines events and errors for common lifecycle events such as successful and failed transfers, and successful and failed initialization.

decl_event!(
	pub enum Event<T>
	where
		AccountId = <T as system::Trait>::AccountId,
	{
		/// Token was initialized by user
		Initialized(AccountId),
		/// Tokens successfully transferred between users
		Transfer(AccountId, AccountId, u64), // (from, to, value)
	}
);

decl_error! {
	pub enum Error for Module<T: Trait> {
		/// Attempted to initialize the token after it had already been initialized.
		AlreadyInitialized,
		/// Attempted to transfer more funds than were available
		InsufficientFunds,
	}
}

Initializing the Token

In order for the token to be useful, some accounts need to own it. There are many possible ways to initialize a token including genesis config, claims process, lockdrop, and many more. This pallet will use a simple process where the first user to call the init function receives all of the funds. The total supply is hard-coded in the pallet in a fairly naive way: It is specified as the default value in the decl_storage! block.

fn init(origin) -> DispatchResult {
	let sender = ensure_signed(origin)?;
	ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized);

	<Balances<T>>::insert(sender, Self::total_supply());

	Init::put(true);
	Ok(())
}

As usual, we first check for preconditions. In this case that means making sure that the token is not already initialized. Then we do any mutation necessary.

Transferring Tokens

To transfer tokens, a user who owns some tokens calls the transfer method specifying the recipient and the amount of tokens to transfer as parameters.

We again check for error conditions before mutating storage. In this case it is not necessary to check whether the token has been initialized. If it has not, nobody has any funds and the transfer will simply fail with InsufficientFunds.

fn transfer(_origin, to: T::AccountId, value: u64) -> DispatchResult {
	let sender = ensure_signed(_origin)?;
	let sender_balance = Self::get_balance(&sender);
	let receiver_balance = Self::get_balance(&to);

	// Calculate new balances
	let updated_from_balance = sender_balance.checked_sub(value).ok_or(<Error<T>>::InsufficientFunds)?;
	let updated_to_balance = receiver_balance.checked_add(value).expect("Entire supply fits in u64; qed");

	// Write new balances to storage
	<Balances<T>>::insert(&sender, updated_from_balance);
	<Balances<T>>::insert(&to, updated_to_balance);

	Self::deposit_event(RawEvent::Transfer(sender, to, value));
	Ok(())
}

Don't Panic!

When adding the incoming balance, notice the peculiar .expect method. In a Substrate runtime, you must never panic. To encourage careful thinking about your code, you use the .expect method and provide a proof of why the potential panic will never happen.