Skip to main content

Non-Fungible Token

Source Code

Open in Codespaces

In the world of digital assets, not all tokens are alike. This becomes important in situations like real estate, voting rights, or collectibles, where some items are valued more than others due to their usefulness, rarity, etc. On Stellar, you can create non-fungible tokens (NFTs), where each token is unique and represents something distinct, with ownership tracked through Soroban smart contracts.

Overview

The non-fungible module provides three different NFT variants that differ in how certain features like ownership tracking, token creation and destruction are handled:

  1. Base: Contract variant that implements the base logic for the NonFungibleToken interface. Suitable for most use cases.
  2. Consecutive: Contract variant for optimized minting of batches of tokens. Builds on top of the base variant, and overrides the necessary functions from the Base variant.
  3. Enumerable: Contract variant that allows enumerating the tokens on-chain. Builds on top of the base variant, and overrides the necessary functions from the Base variant.

These three variants share core functionality and a common interface, exposing identical contract functions as entry-points. However, composing custom flows must be handled with extra caution. That is required because of the incompatible nature between the business logic of the different NFT variants or the need to wrap the base functionality with additional logic.

Run the Example

First go through the Setup process to get your development environment configured, then clone the OpenZeppelin Stellar Contracts repository:

git clone https://github.com/OpenZeppelin/stellar-contracts

Or, skip the development environment setup and open this example in GitHub Codespaces or Code Anywhere.

To run the tests for the example, navigate to the nft-sequential-minting directory, and use cargo test.

cd examples/nft-sequential-minting
cargo test

Code

note

This example demonstrates how to use the OpenZeppelin Stellar Contracts library to create a non-fungible token. The library provides pre-built, audited implementations that follow best practices.

examples/nft-sequential-minting/src/contract.rs
use soroban_sdk::{contract, contractimpl, Address, Env, String};
use stellar_access::ownable::{self as ownable, Ownable};
use stellar_macros::{default_impl, only_owner};
use stellar_tokens::non_fungible::{
burnable::NonFungibleBurnable, Base, ContractOverrides, NonFungibleToken,
};

#[contract]
pub struct NonFungibleTokenContract;

#[contractimpl]
impl NonFungibleTokenContract {
pub fn __constructor(e: &Env, owner: Address) {
// Set token metadata
Base::set_metadata(
e,
String::from_str(e, "www.example.com"),
String::from_str(e, "My NFT Collection"),
String::from_str(e, "MNFT"),
);

// Set the contract owner
ownable::set_owner(e, &owner);
}

#[only_owner]
pub fn mint(e: &Env, to: Address) -> u32 {
Base::sequential_mint(e, &to)
}
}

#[default_impl]
#[contractimpl]
impl NonFungibleToken for NonFungibleTokenContract {
type ContractType = Base;
}

#[default_impl]
#[contractimpl]
impl NonFungibleBurnable for NonFungibleTokenContract {}

#[default_impl]
#[contractimpl]
impl Ownable for NonFungibleTokenContract {}

Ref: https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/nft-sequential-minting

How it Works

This example demonstrates how to create a non-fungible token using the OpenZeppelin Stellar Contracts library. The library provides pre-built, audited implementations that ensure security and follow industry best practices.

The contract implements several key features:

  1. Sequential Minting: Automatically assigns sequential token IDs starting from 1
  2. Ownership Control: Uses the Ownable pattern for administrative functions
  3. Burnable Tokens: Allows token holders to burn their tokens
  4. Secure Minting: Only the contract owner can mint new tokens

By leveraging the OpenZeppelin library, developers can focus on their specific business logic while relying on battle-tested implementations for core NFT functionality.

Using OpenZeppelin Library Components

The OpenZeppelin Stellar Contracts library provides modular components that can be easily composed together:

  • Base: Core non-fungible token functionality with standard NFT operations
  • Ownable: Access control pattern for administrative functions
  • NonFungibleBurnable: Extension that allows tokens to be burned
  • Macros: #[only_owner] for declarative access control

The #[default_impl] macro automatically generates the standard implementations, reducing boilerplate code while maintaining full compatibility with the NFT standards.

Enhanced Security Features

This example showcases several security features built into the OpenZeppelin library:

Access Control: The #[only_owner] macro ensures that only the designated owner can perform administrative actions like minting new tokens.

Sequential ID Generation: The library handles secure token ID generation, preventing collisions and ensuring uniqueness.

Secure Defaults: The library implements secure defaults for all NFT operations, including proper authorization checks and event emissions.

tip

The OpenZeppelin Stellar Contracts library has been audited and follows industry best practices. Using these pre-built components reduces the risk of security vulnerabilities in your NFT contracts.

Usage

We'll use an NFT to track game items, each having their own unique attributes. Whenever one is to be awarded to a player, it will be minted and sent to them. Players are free to keep or burn their token or trade it with other people as they see fit. Please note any account can call award_item and we might want to implement access control to restrict who can mint.

Here's what a contract for tokenized items might look like:

use soroban_sdk::{contract, contractimpl, Address, Env, String};
use stellar_macros::default_impl;
use stellar_tokens::non_fungible::{
burnable::NonFungibleBurnable,
Base, ContractOverrides, NonFungibleToken,
};

#[contract]
pub struct GameItem;

#[contractimpl]
impl GameItem {
pub fn __constructor(e: &Env) {
Base::set_metadata(
e,
String::from_str(e, "www.mygame.com"),
String::from_str(e, "My Game Items Collection"),
String::from_str(e, "MGMC"),
);
}

pub fn award_item(e: &Env, to: Address) -> u32 {
// access control might be needed
Base::sequential_mint(e, &to)
}
}

#[default_impl]
#[contractimpl]
impl NonFungibleToken for GameItem {
type ContractType = Base;
}

#[default_impl]
#[contractimpl]
impl NonFungibleBurnable for GameItem {}

Tests

The OpenZeppelin example includes comprehensive tests that verify both standard NFT functionality and the additional security features.

Key test scenarios include:

  • Token Operations: Testing mint, transfer, burn, and approval functions
  • Access Control: Verifying only the owner can perform administrative actions
  • Token ID Generation: Ensuring sequential IDs are generated correctly
  • Authorization: Confirming proper authentication requirements for each function
  • Edge Cases: Testing boundary conditions and error scenarios

To run the tests:

cd examples/nft-sequential-minting
cargo test

The tests demonstrate how the OpenZeppelin library handles complex scenarios while maintaining security and proper access control.

Build the Contract

To build the contract, use the stellar contract build command.

stellar contract build

A .wasm file should be outputted in the target directory:

target/wasm32v1-none/release/nft_sequential_minting.wasm