12 - PSP34 - Non-Fungible Token (ERC721 equivalent)

In this tuturial we will see how to implement the PSP34 Non-Fungible Token, an ERC721 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_psp34

Use OpenBrush

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

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

[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(PSP34)]
#[openbrush::contract]
pub mod lucky_psp34 {

    use openbrush::contracts::psp34;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
    	#[storage_field]
	psp34: psp34::Data,
    }
    
    impl LuckyPsp34 {
        #[ink(constructor)]
        pub fn new() -> Self {
            Self::default()
        }
    }
}

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

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

PSP34 - Functions

psp34::collectionId()
psp34::totalSupply()
psp34::balanceOf(owner)
psp34::ownerOf(tokenId)
psp34::transfer(to, tokenId, data)
psp34::allowance(owner, operator, tokenId)
psp34::approve(operator, tokenId, approved)

collectionId() → Bytes

Returns the token collection id.

totalSupply() → Balance

Returns the number of tokens in existence.

balanceOf(owner: AccountId) → Balance

Returns the number of tokens in owner's account.

ownerOf(tokenId: Id) → AccountId

Returns the owner of the tokenId token.

Requirements: tokenId must exist.

transfer(to: AccountId, tokenId: Id, data: String)

Move tokenId token from caller to to.

Emits a Transfer event.

allowance(owner: AccountId, operator: AccountId, tokenId: Id) → bool

Returns true if operator is allowed to transfer tokenId token on behalf of owner.

This is false by default.

This value changes when approve is called.

Note: It will be better to rename this method approved.

approve(operator: AccountId, tokenId: Id, approved: bool)

Gives permission to operator to transfer tokenId token to another account.

Requirements:

  • The caller must own the token or be an approved operator.
  • tokenId must exist.

Emits an Approval event.

Note: The method transfer_from is missing. The developer must implement himself this method and use the allowance method to check if the caller is allowed to transfer the token on behalf of owner! It would be better if the method transfer_from is built-in.

Extensions

You can also use extensions for PSP34 token:

  • PSP34Metadata: Provides the ability to add extra information to the token, such as a symbol, a name or an URI or any attributes
  • PSP34Mintable: Allows PSP34 tokens to be minted.
  • PSP34Burnable: Allows PSP34 tokens to be burned.
  • PSP34Enumerable: Allows PSP34 tokens to be enumerated.

PSP34 Metadata

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

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

#[openbrush::implementation(PSP34, PSP34Metadata)]
#[openbrush::contract]
pub mod lucky_psp34 {

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

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp34: psp34::Data,
        #[storage_field]
        metadata: metadata::Data,
    }
    
    impl LuckyPsp34 {
        #[ink(constructor)]
        pub fn new(id: Id, name: String, symbol: String, uri: String) -> Self {
           let mut instance = Self::default();
           metadata::Internal::_set_attribute(&mut instance, id.clone(), "name".to_string(), name);
           metadata::Internal::_set_attribute(&mut instance, id.clone(), "symbol".to_string(), symbol);
           metadata::Internal::_set_attribute(&mut instance, id.clone(), "uri".to_string(), uri);

           instance
       }
    }
}

PSP34 Metadata - Functions

psp34Metadata::getAttribute(tokenId, attribute)

getAttribute**(tokenId: Id, attribute: String) → Option**

Returns the value of attribute attribute of the tokenId token.

PSP34 Mintable

Extends the implementation of PSP34 Mintable

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

#[openbrush::implementation(PSP34, PSP34Mintable)]
#[openbrush::contract]
pub mod lucky_psp34 {

    use openbrush::contracts::psp34;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp34: psp34::Data,
    }
    
    impl LuckyPsp34 {
        #[ink(constructor)]
        pub fn new() -> Self {
           Self::default()
       }
    }
}

PSP34 Mintable - Functions

psp34Mintable::mint(account, tokenId)

mint(account: AccountId, tokenId: Id)

Mints tokenId and transfers it to account.

PSP34 Burnable

Extends the implementation with PSP34Burnable

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

#[openbrush::implementation(PSP34, PSP34Burnable)]
#[openbrush::contract]
pub mod lucky_psp34 {

    use openbrush::contracts::psp34;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp34: psp34::Data,
    }
    
    impl LuckyPsp34 {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {
           Self::default()
       }
    }
}

PSP34 Burnable - Functions

psp34Burnable::burn(owner, tokenId)

burn(owner: AccountId, tokenId: Id)

Destroys tokenId token owned by owner. The approval is cleared when the token is burned.

Requirements:

  • tokenId must exist and the owner must be the owner of the token

Emits a Transfer event.

PSP34 Enumerable

Extends the implementation with PSP34Enumerable

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

#[openbrush::implementation(PSP34, PSP34Enumerable)]
#[openbrush::contract]
pub mod lucky_psp34 {

    use openbrush::contracts::psp34;
    use openbrush::contracts::psp34::extensions::enumerable;
    use openbrush::traits::Storage;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Contract {
        #[storage_field]
        psp34: psp34::Data,
        #[storage_field]
        enumerable: enumerable::Data,
    }
    
    impl LuckyPsp34 {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {
           Self::default()
       }
    }
}

PSP34 Enumerable - Functions

psp34Enumerable::tokenByIndex(index)
psp34Enumerable::ownersTokenByIndex(owner, index)

ownersTokenByIndex(owner: AccountId, index: u256) → Id

Returns a token ID owned by owner at a given index of its token list. Use along with balanceOf to enumerate all of owner's tokens.

tokenByIndex(index: u256) → Id

Returns a token ID at a given index of all the tokens stored by the contract. Use along with totalSupply to enumerate all tokens.

Events

By default, no event is emitted.

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

_emit_transfer_event(from: Option, to: Option, tokenId: Id)

This function is called when :

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

_emit_approval_event(owner: AccountId, spender: AccountId, tokenId: Id, approved: bool)

This function is called each time the allowance is updated.

For example:

#[overrider(psp34::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(Psp34Transfer{contract_id, from, to, amount});
}

#[overrider(psp34::Internal)]
fn _emit_approval_event(&self, owner: AccountId, spender: AccountId, amount: Balance) {
    let contract_id = self.env().account_id();
    self.env().emit_event(Psp34Approval { 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(PSP34Mintable)]
#[modifiers(ownable::only_owner)]
fn mint() {}

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

Conclusion

OpenBrush provides the default implementation of traits for the PSP34 Standard and it is easy for the developers to create a PSP34 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/tuto12

Source

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

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!