What is PSP-34?
PSP34 is Polkadot’s native NFT standard, designed to address ERC721’s limitations while leveraging Polkadot’s strengths.
What is OpenBrush?
OpenBrush to Polkadot is what OpenZeppelin is to Ethereum. A library of secure, reusable smart contracts for Ink! (instead of Solidity). It provides:
- Pre-built Standards: PSP22 (fungible tokens), PSP34 (NFTs), PSP37 (multi-tokens).
- Extensions: Royalties, enumerable NFTs, batch transfers.
- Security Audits: Community-vetted code to prevent vulnerabilities (no more reentrancy hacks!).
Feature | ERC721 (Ethereum) | PSP34 (Polkadot) |
---|---|---|
Language | Solidity | Rust (via ink!) |
Security | Runtime checks | Compile-time guarantees (no overflows) |
Cross-Chain | Requires bridges (e.g., LayerZero) | Native XCM support |
Metadata | Optional (tokenURI ) |
Built-in (via PSP34Metadata extension) |
Batch Ops | Not natively supported | transfer_batch (gas-efficient) |
-
Architectural Mismatch:
- ERC721 relies on EVM/Solidity, while Polkadot uses Wasm/Rust. Rewriting ERC721 for ink! would be inefficient.
-
Cross-Chain Needs:
- PSP34 natively supports XCM (cross-chain messaging), enabling NFTs to move between parachains without bridges.
-
Enhanced Security:
- Rust’s ownership model prevents common ERC721 pitfalls (e.g., reentrancy, integer overflows).
-
Governance & Upgradability:
- PSP34 integrates with Polkadot’s on-chain governance, allowing standards to evolve without hard forks.
-
Scalability:
- Batch operations (e.g., minting 100 NFTs in one TX) reduce gas costs — critical for parachains like Unique Network.
Cardano comparison with CIP25
Aspect | Cardano (CIP25) | Polkadot (PSP34) |
---|---|---|
Standard | CIP25 (metadata-centric) | PSP34 (behavior-centric) |
Cross-Chain | Limited (requires bridges) | Native via XCM |
Royalties | Post-mint marketplace enforcement | Enforced at contract level |
While the code demonstrates core NFT functionalities (mint, burn, transfer), deploying it to live networks is currently unfeasible due to OpenBrush’s outdated dependencies. We’ll focus on understanding the code and testing it locally.
lib.rs
#![cfg_attr(not(feature = "std"), no_std, no_main)]
// OpenBrush macros to auto-implement PSP34 traits and extensions
#[openbrush::implementation(PSP34, PSP34Burnable, PSP34Mintable)]
#[openbrush::contract]
pub mod psp34_ob {
use openbrush::traits::Storage;
// Storage structure for the NFT contract
#[ink(storage)]
#[derive(Default, Storage)]
pub struct Contract {
// OpenBrush's macro injects PSP34 storage logic (owners, token IDs)
#[storage_field]
psp34: psp34::Data, // Holds NFT ownership and metadata
}
impl Contract {
// Constructor: Mints NFT #1 to the deployer
#[ink(constructor)]
pub fn new() -> Self {
let mut _instance = Self::default();
// Internal helper to mint token ID 1 (type U8) to the caller
psp34::Internal::_mint_to(
&mut _instance,
Self::env().caller(),
Id::U8(1) // ID format (U8, U16, U32, etc.)
).expect("Can mint");
_instance
}
}
}
Cargo.toml
[package]
name = "psp34_ob"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]
edition = "2021"
[dependencies]
ink = { version = "4.2.1", default-features = false }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true }
openbrush = { version = "4.0.0-beta", default-features = false, features = ["psp34"] }
[lib]
name = "psp34_ob"
path = "lib.rs"
[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"openbrush/std",
]
ink-as-dependency = []
-
PSP34 Extensions:
PSP34Burnable
: Allows burning NFTs.PSP34Mintable
: Enables minting new tokens (like ERC721’s_mint
).
-
Storage:
psp34::Data
: Auto-injected by OpenBrush to track ownership (equivalent to ERC721’smapping(uint256 => address)
).
-
ID Types:
Id::U8(1)
: Tokens can use integer IDs (U8, U16, etc.) or bytes.
-
Tests:
- Simulate blockchain interactions (minting, transfers) without a live network.
-
Ink! 3.x vs 4.x: OpenBrush relies on older ink! versions (3.x), while modern parachains require 5.x.
-
WASM Validation Errors:
Error: This contract file is not in a valid format.
Parachains like Astar reject contracts built with deprecated toolchains and even downgrading ink! doesn't help.
Since deployment is blocked, unit tests are your only validation tool:
// Unit tests (the only reliable way to validate this contract)
#[cfg(test)]
mod tests {
use super::*;
use ink::env::test;
// Test 1: Verify initial minting in constructor
#[ink::test]
fn test_new() {
let contract = Contract::new();
let caller = test::default_accounts::<ink::env::DefaultEnvironment>().alice;
// Check if Alice (caller) has 1 NFT
assert_eq!(PSP34::balance_of(&contract, caller), 1);
}
// Test 2: Mint a new NFT (ID #4)
#[ink::test]
fn test_mint() {
let mut contract = Contract::new();
let caller = test::default_accounts::<ink::env::DefaultEnvironment>().alice;
// Mint token ID 4 to Alice
psp34::Internal::_mint_to(&mut contract, caller, Id::U8(4)).expect("Can mint");
// Alice now owns 2 NFTs (IDs 1 and 4)
assert_eq!(PSP34::balance_of(&contract, caller), 2);
}
// Test 3: Burn NFT #1
#[ink::test]
fn test_burn() {
let mut contract = Contract::new();
let caller = test::default_accounts::<ink::env::DefaultEnvironment>().alice;
// Burn token ID 1
psp34::Internal::_burn_from(&mut contract, caller, Id::U8(1)).expect("Can burn");
// Alice's balance drops to 0
assert_eq!(PSP34::balance_of(&contract, caller), 0);
}
// Test 4: Transfer NFT #1 to Bob
#[ink::test]
fn test_transfer() {
let mut contract = Contract::new();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
let alice = accounts.alice;
let bob = accounts.bob;
// Transfer token ID 1 from Alice to Bob
PSP34::transfer(&mut contract, bob, Id::U8(1), b"".to_vec()).expect("Can transfer");
// Verify balances
assert_eq!(PSP34::balance_of(&contract, alice), 0);
assert_eq!(PSP34::balance_of(&contract, bob), 1);
}
}
- OpenBrush is not mantained: Useful for learning, but avoid for production.
- Stick to Tests: Validate logic locally until tooling stabilizes.
- Ditch OpenBrush: Write the standard from scratch (if you cannot wait an updated version).