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 theowner
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
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