Sui NFT Tutorial Advanced: Dynamic Fields, Soulbound Tokens & Kiosk Commerce in Move

12 min read

Advanced Sui Move NFT patterns including dynamic fields, soulbound tokens, and Kiosk commerce visualized as holographic blockchain objects
  • On Sui, every object with a UID is already an NFT — no special standard needed, unlike Ethereum’s ERC-721.
  • Dynamic fields let you add, change, or remove attributes on a live NFT without redeploying the contract.
  • Soulbound NFTs are built by removing the store ability from a Move struct — one line of code locks the token to its owner forever.
  • Sui Kiosk is the native on-chain commerce layer that enforces royalties automatically, no marketplace cooperation required.
  • Programmable Transaction Blocks (PTBs) let you mint, update metadata, and list in a single atomic step — a capability other chains cannot match.
  • Chialisp developers will recognize the core pattern: controlling transfer conditions at the contract level mirrors how Chia puzzles gate coin spends.

This sui nft tutorial advanced picks up exactly where the beginner mint-and-transfer guide left off. You already know how to create a basic struct with key, store, call object::new, and use transfer::public_transfer. Now you are going to learn the four patterns that separate a toy NFT from a production-ready one: dynamic fields that make your token evolve, soulbound logic that ties a token to one wallet forever, Kiosk integration that lets you sell with enforceable royalties, and Programmable Transaction Blocks that bundle all of it into a single call.

Why Sui’s Object Model Changes Everything for NFTs

Before diving into code, it helps to understand what makes Sui different at a design level. On most blockchains, an NFT is a record inside a mapping. The contract owns a ledger, and you own an entry in it. On Sui, the NFT is an object — it lives at its own address, you own it outright, and you interact with it directly. On Sui, every object is already a unique, non-fungible, owned asset by definition. There is no ERC-721 registry to inherit from. This is why the official docs state plainly: “On Sui, everything is an object. Moreover, everything is a non-fungible token (NFT) as its objects are unique, non-fungible, and owned.”[1]

For miners who have worked with Chia, this feels familiar. In Chialisp, a coin is a distinct entity with its own puzzle hash, not a balance entry in a shared table. Spending a coin requires solving its puzzle — the rules live on the coin itself. Sui works the same way at the object level. Move abilities (keystorecopydrop) are the rules that govern what you can do with an object, just as Chialisp conditions govern what a coin spend can create or require. When you understand that parallel, the rest of this tutorial clicks into place. Explore how Chialisp’s smart contract model compares to Move concepts in our deep dive on Chialisp and next-gen smart contracts.

“The most successful projects on Sui are not clones of existing DeFi protocols. They are native products that cannot be built elsewhere.”— Evan Cheng, Co-Founder & CEO of Mysten Labs, in an interview with CoinDesk, December 2024[2]

That insight is a direct challenge to developers: if you are building an NFT project that could run identically on Ethereum, you are leaving Sui’s best features on the table. This tutorial shows you what native Sui NFT design actually looks like.

Quick Reference: Which Advanced Pattern Do You Need?

GoalPatternKey Move ConceptBest For
NFT that gains XP or levels upDynamic Fieldsdynamic_field::add / borrow_mutGame assets, evolving PFPs
NFT locked to one walletSoulboundRemove store abilityIdentity, credentials, badges
Sell NFTs with enforced royaltiesSui Kiosk + TransferPolicykiosk::placekiosk::listCollections, marketplaces
NFT rented temporarilyKiosk Rental AppERC-4907-style rental via Kiosk AppsGame power-ups, subscriptions
Mint + list in one stepProgrammable Transaction BlocksTransactionBlock via Sui SDKLaunchpads, batch operations
NFT that owns other NFTsComposability / Dynamic Object Fieldsdynamic_object_field::addInventory systems, bundles

Dynamic Fields: Building NFTs That Actually Evolve

When you define a Move struct, all fields are fixed at publish time. If you want a level field later, you would normally need to redeploy. Dynamic fields solve this entirely. They let you attach new key-value pairs to any object after it has been created, and you pay gas only when you access them — not for every transaction that touches the NFT.[3]

Regular Dynamic Fields vs. Dynamic Object Fields

Sui offers two flavors. A plain dynamic field stores a value that is wrapped inside the parent object and is not directly addressable on-chain by explorers. A dynamic object field stores a child that is itself a Sui object (has key), meaning it gets its own address and remains visible in wallets and indexers. For game items like a sword or a piece of armor that a player wants to see in their inventory, use dynamic object fields. For simple scalar attributes like level or experience points, a plain dynamic field is cheaper and easier.

Here is the full pattern for an evolving game-character NFT. Notice that the base struct stays small — it holds only the UID and name. All mutable stats are attached afterward as dynamic fields.

module my_game::hero;

use std::string::{Self, String};
use sui::dynamic_field;
use sui::transfer;
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};

/// The base NFT — keep it lean
public struct Hero has key, store {
    id: UID,
    name: String,
}

/// Mint a fresh Hero with no stats yet
public fun mint(name: vector<u8>, ctx: &mut TxContext): Hero {
    Hero {
        id: object::new(ctx),
        name: string::utf8(name),
    }
}

/// Add a "level" field dynamically after mint
public fun set_level(hero: &mut Hero, level: u64) {
    // If level already exists, update it
    if (dynamic_field::exists_(&hero.id, b"level")) {
        let current = dynamic_field::borrow_mut<vector<u8>, u64>(
            &mut hero.id, b"level"
        );
        *current = level;
    } else {
        dynamic_field::add(&mut hero.id, b"level", level);
    }
}

/// Read the level field
public fun get_level(hero: &Hero): u64 {
    if (dynamic_field::exists_(&hero.id, b"level")) {
        *dynamic_field::borrow<vector<u8>, u64>(&hero.id, b"level")
    } else {
        0
    }
}

/// Transfer the Hero to another address
public fun send(hero: Hero, recipient: address) {
    transfer::public_transfer(hero, recipient);
}

The key line is dynamic_field::add(&mut hero.id, b"level", level). The first argument is a mutable reference to the object’s UID, the second is the field name (which can be any copy + drop + store value), and the third is the value. You can add as many fields as you want after mint — XP, achievements, equipped items, win counts — without ever touching the struct definition.

Gas-Aware Design for Dynamic Fields

A common mistake is attaching dozens of fields to a single root NFT. Every read or write to a dynamic field requires fetching the child object from the object store. Keep your root NFT small and push all mutable state into dynamic fields or child objects. This way you pay gas only for the data you actually access in a given transaction. For heavy or rarely-accessed data like full match histories or complete inventory lists, consider a separate child object that is attached via a dynamic object field. You can include it in a transaction only when the user explicitly accesses it.

Soulbound NFTs: Locking a Token to Its Owner

The concept of soulbound tokens was introduced by Ethereum co-founder Vitalik Buterin as a way to represent identity, credentials, and achievements that should not be traded. On Sui, creating a soulbound NFT is a one-line change. You remove the store ability from your struct. Without store, the object cannot be placed inside another object and transfer::public_transfer becomes unavailable to any caller — only the defining module can invoke transfer::transfer, and even then only under rules you write yourself.[4]

The Soulbound Struct and Why It Works

Here is a minimal soulbound badge for a game achievement:

module my_game::achievement_badge;

use std::string::{Self, String};
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::event;

/// No `store` ability = soulbound
public struct AchievementBadge has key {
    id: UID,
    achievement: String,
    awarded_at: u64,
}

public struct BadgeAwarded has copy, drop {
    badge_id: sui::object::ID,
    recipient: address,
    achievement: String,
}

/// Only this module can mint badges
public fun award(
    achievement: vector<u8>,
    recipient: address,
    clock_ms: u64,
    ctx: &mut TxContext
) {
    let badge = AchievementBadge {
        id: object::new(ctx),
        achievement: string::utf8(achievement),
        awarded_at: clock_ms,
    };

    event::emit(BadgeAwarded {
        badge_id: object::id(&badge),
        recipient,
        achievement: badge.achievement,
    });

    // transfer::public_transfer is unavailable — we use module-level transfer
    transfer::transfer(badge, recipient);
}

/// No transfer function exposed = fully soulbound
/// Uncomment below only if you want conditional transfer logic
// public fun transfer_badge(badge: AchievementBadge, new_owner: address) {
//     // Add custom rules here, e.g. only admin can reassign
//     transfer::transfer(badge, new_owner);
// }

Once store is removed, no external code can transfer this badge. It is permanently bound to its recipient’s wallet.[5] This is extremely powerful for use cases like game season passes, identity documents, loyalty tiers, or proof-of-participation certificates. It is also where Sui diverges sharply from Ethereum — on EVM chains, preventing transfer usually means adding complex checks inside every transfer function. On Sui, the type system enforces it at compile time.

The Chialisp comparison here is direct: in Chia, you gate a coin spend by writing conditions that must be satisfied — “this puzzle can only be solved if the solution contains the owner’s signature.” In Move, the soulbound pattern gates a transfer at the type level. Both approaches push trust out of marketplaces and into the protocol itself. Neither requires you to trust a platform to honor the rules.

Conditional Transfers: A Middle Ground

Soulbound does not have to mean non-transferable forever. Because only your module can call transfer::transfer on a no-store struct, you can add your own custom transfer function with whatever logic you want. You might allow transfer only if a signature from an admin address is present, or only after a cooldown period, or only to addresses on a whitelist. This gives you the same enforcement guarantee while allowing controlled mobility — perfect for credentialing systems where a user can move their identity token to a new wallet after completing a verification step.

FeatureStandard NFT (has key, store)Soulbound NFT (has key only)
Freely transferableYes, anyone can call public_transferNo — public_transfer blocked by type system
Storable inside other objectsYesNo — cannot be wrapped in another object
Composable in KioskYes, can be listed and soldNo — cannot enter Kiosk for trading
Custom transfer logicOptional overrideRequired — only module-level transfer works
Dynamic fieldsYesYes — can still evolve via dynamic fields
Ideal use caseCollectibles, game items, PFPsBadges, credentials, season passes, identity

Sui Kiosk: On-Chain Commerce With Enforced Royalties

Sui Kiosk is one of the most important features in the entire Sui ecosystem and one that has no real equivalent on other chains. It is a native, decentralized system built directly into the Sui framework that lets creators set royalty and transfer rules that every marketplace must follow — not as a convention, but as a protocol-level requirement.[6]

How Kiosk Works at the Contract Level

A Kiosk is a shared object that an individual user owns via a KioskOwnerCap. When you place an NFT into a Kiosk, you retain full ownership of the asset. When a buyer purchases the item, a TransferRequest is created and must pass through the NFT type’s TransferPolicy before the sale completes. The creator defines the TransferPolicy once — adding rules like a royalty percentage or a locking rule — and those rules apply to every single trade of that asset type, on every marketplace, forever. No marketplace can bypass them because the Sui runtime will abort any transaction that tries to confirm a TransferRequest without satisfying the policy.[7]

Here is the minimum code to set up a basic Kiosk collection with a royalty rule:

module my_collection::art;

use std::string::String;
use sui::url::{Self, Url};
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
use sui::transfer_policy::{Self, TransferPolicy, TransferPolicyCap};
use sui::package;
use sui::transfer;

/// Your NFT type — must have store to enter Kiosk
public struct ArtPiece has key, store {
    id: UID,
    name: String,
    image_url: Url,
}

/// One-time witness for Publisher creation
public struct ART has drop {}

fun init(otw: ART, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    transfer::public_transfer(publisher, ctx.sender());
}

/// Mint a new piece
public fun mint(
    name: String,
    image_url: vector<u8>,
    ctx: &mut TxContext
): ArtPiece {
    ArtPiece {
        id: object::new(ctx),
        name,
        image_url: url::new_unsafe_from_bytes(image_url),
    }
}

/// Create and share the TransferPolicy — call this ONCE after deploy
public fun create_policy(
    publisher: &sui::package::Publisher,
    ctx: &mut TxContext
) {
    let (policy, cap) = transfer_policy::new<ArtPiece>(publisher, ctx);
    // Share the policy so marketplaces can use it
    transfer::public_share_object(policy);
    // Keep the cap — you control the rules
    transfer::public_transfer(cap, ctx.sender());
}

After deploying, you add a royalty rule to the TransferPolicy using the Kiosk SDK or the CLI. Once that is set, every secondary sale of your ArtPiece type will route the royalty to you automatically. No trust required. No platform agreement needed.

NFT Rental via Kiosk Apps

Sui also supports NFT rental through the Kiosk Apps standard — a pattern that closely mirrors the ERC-4907 rental standard from Ethereum. Instead of transferring ownership, a Kiosk App can allow a user to borrow an NFT for a defined period under rules set by the owner. When the rental expires, the object returns to the kiosk automatically. This is especially useful for gaming scenarios where a player wants to use a high-level item for a tournament without permanently giving it up, or for subscription-based content access where the token grants access for a limited window.

Case Study: Prime Machin and On-Chain Dynamic Art

The Prime Machin collection on Sui demonstrates the real-world power of these patterns. Holders can spend KOTO tokens to transform their black-and-white NFTs into color — a one-way, on-chain action that permanently changes the object’s state. As the first PFP collection on Sui to store hand-drawn raster art fully on-chain at 4K resolution, Prime Machin takes direct advantage of Sui’s object-centric architecture and dynamic NFT capabilities, achieving storage costs that its team reports as 100x cheaper than comparable Solana collections.

Programmable Transaction Blocks: Doing More in One Call

Programmable Transaction Blocks (PTBs) are Sui’s answer to the question developers on every chain have asked: “Why do I need five transactions to do what should be one action?” A PTB lets you chain multiple Move calls, coin operations, and object transfers into a single atomic transaction. If any step fails, the entire block rolls back. This means you can mint an NFT, attach a dynamic field, place it in a Kiosk, and list it for sale — all in one user confirmation, one gas payment.[1]

Here is what a mint-and-list PTB looks like in pseudocode using the Sui TypeScript SDK:

import { TransactionBlock } from '@mysten/sui.js/transactions';

const tx = new TransactionBlock();

// Step 1: Mint the NFT
const [nft] = tx.moveCall({
    target: `${PACKAGE_ID}::art::mint`,
    arguments: [tx.pure("Rare Piece #001"), tx.pure(IMAGE_URL_BYTES)],
});

// Step 2: Create a personal Kiosk (if not already created)
const [kiosk, kioskCap] = tx.moveCall({
    target: '0x2::kiosk::new',
    arguments: [],
});

// Step 3: Place the NFT in the Kiosk
tx.moveCall({
    target: '0x2::kiosk::place',
    arguments: [kiosk, kioskCap, nft],
    typeArguments: [`${PACKAGE_ID}::art::ArtPiece`],
});

// Step 4: List it for sale (in MIST, Sui's smallest unit)
tx.moveCall({
    target: '0x2::kiosk::list',
    arguments: [kiosk, kioskCap, tx.pure(NFT_ID), tx.pure(1_000_000_000n)],
    typeArguments: [`${PACKAGE_ID}::art::ArtPiece`],
});

// Share the kiosk and transfer the cap
tx.moveCall({ target: '0x2::transfer::public_share_object', arguments: [kiosk] });
tx.transferObjects([kioskCap], tx.pure(senderAddress));

// Submit — all four steps execute atomically
const result = await suiClient.signAndExecuteTransactionBlock({ transactionBlock: tx, signer });

The output of Step 1 flows directly into Step 3 as an input — the NFT never touches your wallet between operations. PTBs are arguably Sui’s single most powerful developer feature, enabling workflows that would require custom router contracts on every other chain. For miners building tools or launchpads, this means you can build a one-click launch experience without any multi-step UX complexity on the user side.

Composability: NFTs That Own Other NFTs

Sui’s final advanced trick is composability — the ability for one Sui object to contain other Sui objects as children. Using dynamic object fields, a “Character” NFT can own “Weapon” and “Armor” NFTs. The children are addressable as separate objects but logically belong to the parent. When the parent is transferred, the children move with it. When the game logic says “equip this sword,” you call dynamic_object_field::add and the sword becomes a child of the character. When you “unequip” it, you call dynamic_object_field::remove and it becomes a standalone object again.

This is where Sui’s object model truly shines beyond what any account-based chain can offer natively. Building this on Ethereum would require multiple contracts, mapping lookups, and careful state management. On Sui, the data model itself handles ownership and containment at the protocol level.

Summary: Your Advanced Sui NFT Roadmap

You now have the four building blocks of production-grade Sui NFT development. Dynamic fields give your tokens a life beyond mint. Soulbound logic lets you build identity and credentialing systems that no marketplace can compromise. Kiosk gives creators royalty enforcement that is protocol-guaranteed, not convention-based. And PTBs let you bundle any combination of these operations into one user-friendly transaction. Start by extending your existing mint module with a single dynamic field for experience points. Then add the soulbound variant for any achievement tokens in your project. When you are ready to launch publicly, layer in the Kiosk TransferPolicy and wire it up with a PTB-powered mint-and-list flow. Each pattern builds on the last, and together they describe an NFT system that genuinely cannot be built the same way anywhere else.

sui nft tutorial advanced FAQs

What makes this sui nft tutorial advanced compared to a basic mint-and-transfer guide?

This advanced sui nft tutorial covers four patterns that go beyond a basic mint: dynamic fields for evolving state, soulbound tokens that enforce non-transferability at the type level, Sui Kiosk for protocol-enforced royalties, and Programmable Transaction Blocks that chain multiple operations into one atomic step. A beginner guide stops at minting an object and sending it — this guide shows how to build NFTs that behave like real digital assets with real commerce rules.

How do I make an NFT soulbound in Sui Move?

Remove the store ability from your struct definition, leaving only has key. Without storetransfer::public_transfer is unavailable to any external caller, so the token can only move if your module’s own code explicitly calls transfer::transfer. This is enforced by the Move bytecode verifier at compile time, not by runtime checks.

What is Sui Kiosk and why do I need it for an NFT collection?

Sui Kiosk is a native, shared-object commerce system built into the Sui framework that enforces your NFT’s TransferPolicy on every secondary sale. Without Kiosk, a buyer can receive your NFT and resell it on any platform without paying your royalty. With Kiosk, the protocol itself blocks any trade that does not satisfy your rules, regardless of which marketplace facilitates the sale.

Can I add new attributes to an NFT after it has already been minted in this sui nft tutorial advanced?

Yes — this is exactly what dynamic fields are for. You call dynamic_field::add(&mut nft.id, field_name, value) at any point after mint, and the new attribute is stored on-chain attached to the object’s UID. You can also update or remove attributes later with borrow_mut and remove, making dynamic fields the foundation for any evolving NFT.

How are Programmable Transaction Blocks different from a regular Move function call?

A regular Move call executes one function in one transaction. A Programmable Transaction Block (PTB) chains multiple function calls, coin splits, and object transfers into a single atomic transaction where the output of one step becomes the input of the next. If any step fails, the entire block rolls back, eliminating partial-execution bugs and reducing user-facing confirmation steps from many to one.

sui nft tutorial advanced Citations

  1. Sui Documentation — NFT Developer Guide. docs.sui.io/guides/developer/nft
  2. CoinDesk — Evan Cheng: The Architect of Sui’s Object-Oriented Revolution. December 2024.
  3. Sui Documentation — Dynamic Fields. docs.sui.io/concepts/dynamic-fields
  4. Sui Documentation — Soulbound NFT Example. docs.sui.io/guides/developer/nft/nft-soulbound
  5. Sui Blog — All About Soulbound Tokens. blog.sui.io
  6. Sui Documentation — Sui Kiosk Standard. docs.sui.io/standards/kiosk
  7. Sui Blog — All About Kiosks. blog.sui.io