11 - PSP22 - Fungible Token (ERC20 equivalent)

In this tuturial we will see how to implement the PSP22 Fungible Token, an ERC20 equivalent on Astar Network or any Substrate Contracts Node with OpenBrush.

OpenBrush is a library for smart contract development on ink! maintained by the Brushfam team and provides standard contracts based on Polkadot Standard Proposals (PSPs).

Prerequisites

Cargo contract installed: https://polkaverse.com/9768/technical-guide-install-cargo-contract-37738

This tutorial uses cargo 1.72.0 and cargo-contract 3.2.

Create the project

You can use the command “cargo contract new” to create a new contract based on the template “flipper“.

cargo contract new lucky_psp22

Use OpenBrush

Add openbrush in your Cargo.toml and to the std features and enable the psp22 feature.

openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", version = "4.0.0", default-features = false, features=["psp22"] }

[features]
default = ["std"]
std = [
    "ink/std",
    "scale/std",
    "scale-info/std",
    "openbrush/std"
]

Define storage and constructor

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[openbrush::implementation(PSP22)]
#[openbrush::contract]
pub mod lucky_psp22 {

    use openbrush::contracts::psp22;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
    	#[storage_field]
	psp22: psp22::Data,
    }
    
    impl LuckyPsp22 {
        #[ink(constructor)]
        pub fn new(initial_supply: Balance) -> Self {
            let mut instance = Self::default();
            psp22::Internal::_mint(&mut instance, Self::env().caller(), initial_supply).expect("Should mint"); 
            instance
        }
    }
}

And that’s all! It’s really easy to create a PSP22 (ERC20 equivalent) token!

You can now deploy your contract on any Substrate Contracts Node and start playing with it.

PSP22 - Functions

psp22::totalSupply()
psp22::balanceOf(account)
psp22::transfer(to, value)
psp22::allowance(owner, spender)
psp22::approve(spender, value)
psp22::increaseAllowance(spender, value)
psp22::decreaseAllowance(spender, value)
psp22::transferFrom(from, to, value)

totalSupply() → Balance

Returns the value of tokens in existence.

balanceOf(account: AccountId) → Balance

Returns the value of tokens owned by account.

transfer(to: AccountId, value: Balance)

Moves a value amount of tokens from the caller’s account to to.

allowance(owner: AccountId, spender: AccountId) → Balance

Returns the remaining number of tokens that spender will be allowed to spend on behalf of owner through transferFrom. This is zero by default.

This value changes when approve or transferFrom are called.

approve(spender: AccountId, value: Balance)

Sets a value amount of tokens as the allowance of spender over the caller’s tokens.

Important: Beware this method is Vulnerable to Double-Spending: changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering.

You can have more information about this issue in the security review from OpenZepelin: https://blog.openzeppelin.com/openbrush-contracts-library-security-review

To avoid Double-Spending attack, you must use the increaseAllowance and decreaseAllowance functions to modify token allowances for user expenditure.

increaseAllowance(spender: AccountId, value: Balance)

decreaseAllowance(spender: AccountId, value: Balance)

Increase or decrease a value amount of tokens as the allowance of spender over the caller’s tokens.

transferFrom(from: AccountId, to: AccountId, value: Balance)

Moves a value amount of tokens from from to to using the allowance mechanism. value is then deducted from the caller’s allowance.

Fails and returns an error if the caller is not allowed to spend value amount of tokens on behalf of from.

Extensions

You can also use extensions for PSP22 token:

  • PSP22Metadata: Provides the ability to add extra information to the token, such as a symbol and a name.
  • PSP22Mintable: Allows PSP22 tokens to be minted.
  • PSP22Burnable: Allows PSP22 tokens to be burned.
  • PSP22Capped: Allows PSP22 tokens to be capped.

PSP22 Metadata

Extends the implementation with PSP22Metadata, defines the storage and the constructor.

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[openbrush::implementation(PSP22, PSP22Metadata)]
#[openbrush::contract]
pub mod lucky_psp22 {

    use openbrush::contracts::psp22;
    use openbrush::contracts::psp22::extension::metadata;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp22: psp22::Data,
        #[storage_field]
        metadata: metadata::Data,
    }
    
    impl LuckyPsp22 {
        #[ink(constructor)]
        pub fn new(total_supply: Balance, name: Option<String>, symbol: Option<String>, decimal: u8) -> Self {
           let mut instance = Self::default();
           let caller = instance.env().caller();
 
           instance.metadata.name.set(&name);
           instance.metadata.symbol.set(&symbol);
           instance.metadata.decimals.set(&decimal);

           psp22::Internal::_mint_to(&mut instance, caller, total_supply).expect("Should mint total_supply");

           instance
       }
    }
}

PSP22 Metadata - Functions

psp22Metadata::tokenName()
psp22Metadata::tokenSymbol()
psp22Metadata::tokenDecimals()

tokenName() → Option

Returns the name of the token.

tokenSymbol() → Option

Returns the symbol of the token.

tokenDecimals() → Option

Returns the decimals places of the token.

PSP22 Mintable

Extends the implementation of PSP22 Mintable

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[openbrush::implementation(PSP22, PSP22Mintable)]
#[openbrush::contract]
pub mod lucky_psp22 {

    use openbrush::contracts::psp22;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp22: psp22::Data,
    }
    
    impl LuckyPsp22 {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {
           let mut instance = Self::default();
           let caller = instance.env().caller();
 
           psp22::Internal::_mint_to(&mut instance, caller, total_supply).expect("Should mint total_supply");

           instance
       }
    }
}

PSP22 Mintable - Functions

psp22Mintable::mint(account, value)

mint(account: AccountId, value: Balance)

Creates a value amount of tokens and assigns them to account, by transferring it from address(0).

PSP22 Burnable

Extends the implementation with PSP22Burnable

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[openbrush::implementation(PSP22, PSP22Burnable)]
#[openbrush::contract]
pub mod lucky_psp22 {

    use openbrush::contracts::psp22;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp22: psp22::Data,
    }
    
    impl LuckyPsp22 {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {
           let mut instance = Self::default();
           let caller = instance.env().caller();
 
           psp22::Internal::_mint_to(&mut instance, caller, total_supply).expect("Should mint total_supply");

           instance
       }
    }
}

PSP22 Burnable - Functions

psp22Burnable::burn(account, value)

burn(account: AccountId, value: Balance)

Destroys a value amount of tokens from the account, deducting from the caller’s allowance.

Fails and returns an error if the caller is not allowed to spend value amount of tokens on behalf of account.

PSP22 Capped

Extends the implementation with PSP22Capped, defines the storage and the constructor

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[openbrush::implementation(PSP22, PSP22Capped)]
#[openbrush::contract]
pub mod lucky_psp22 {

    use openbrush::contracts::psp22;
    use openbrush::contracts::psp22::extensions::capped;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp22: psp22::Data,
        #[storage_field]
        cap: capped::Data,
    }
    
    impl LuckyPsp22 {
        #[ink(constructor)]
        pub fn new(total_supply: Balance, cap: Balance) -> Self {
           let mut instance = Self::default();
           let caller = instance.env().caller();
            capped::Internal::_init_cap(&mut instance, cap).expect("Should cap the supply");
           psp22::Internal::_mint_to(&mut instance, caller, total_supply).expect("Should mint initial_supply");

           instance
       }
    }
}

PSP22 Capped - Functions

cap()

cap() → Balance

Returns the cap on the token’s total supply.

Events

By default, no event is emitted.

However to keep track of all funds received, you can override the following functions to emit custom events:

_emit_transfer_event(from: Option, to: Option, amount: Balance)

This function is called when :

  • a transfer is done,
  • new tokens are minted,
  • some tokens are burned.

_emit_approval_event(owner: AccountId, spender: AccountId, amount: Balance)

This function is called each time the allowance is updated.

For example:

#[overrider(psp22::Internal)]
fn _emit_transfer_event(&self, from: Option<AccountId>, to: Option<AccountId>, amount: Balance) {
    let contract_id = self.env().account_id();
    self.env().emit_event(Psp22Transfer{contract_id, from, to, amount});
}

#[overrider(psp22::Internal)]
fn _emit_approval_event(&self, owner: AccountId, spender: AccountId, amount: Balance) {
    let contract_id = self.env().account_id();
    self.env().emit_event(Psp22Approval { contract_id, owner, spender, amount });
}

Security

By default, no security is implemented but you can use the modifiers macro to restrict the access.

For example:

#[default_impl(PSP22Mintable)]
#[modifiers(ownable::only_owner)]
fn mint() {}

#[default_impl(PSP22Burnable)]
#[modifiers(ownable::only_owner)]
fn burn() {}

Custom logic

For further, you can override the following functions to add custom logic before and after all token transfers

_before_token_transfer(from: AccountId, to: AccountId, amount: Balance)

_after_token_transfer(from: AccountId, to: AccountId, amount: Balance)

For example:

 #[overrider(psp22::Internal)] 
 fn _before_token_transfer(
    &mut self,
    from: Option<&AccountId>,
    _to: Option<&AccountId>,
    _amount: &Balance,
  ) -> Result<(), PSP22Error> {
    if from == self.banned_account.get() {
      return Err(PSP22Error::InsufficientAllowance)
    }
    Ok(())
  }

Conclusion

OpenBrush provides the default implementation of traits for the PSP22 Standard and it is easy for the developers to create a PSP22 token and customize the business logic.

However, some features could be improved, specially the events emmitting and the security could be included nativaly in the library.

Git repository

You can find the full example here

https://github.com/GuiGou12358/astar-tutorials/tree/main/tuto11

Source

https://learn.brushfam.io/docs/OpenBrush/smart-contracts/PSP22/

https://blog.openzeppelin.com/openbrush-contracts-library-security-review

0
GuiGouPost author

Crypto-enthusiast, Defi & NFT believer, Dotsam Fam Astar Tech Amb & Phala Amb Web2 builder gradually migrating to Web3

Tutorials to write Smart Contracts in Rust and Ink!

0 comments

Tutorials to write Smart Contracts in Rust and Ink!