Sui.

Post

Share your knowledge.

Bekky.
Bekky1762
Jul 25, 2025
Expert Q&A

How to Create Dynamic NFT Collections in Sui Move?

Overview

I’m building a dynamic NFT collection on Sui where tokens can:

  • Evolve traits (e.g., in-game item upgrades)
  • Merge/split (composable NFTs)
  • React to external data (oracles/events)
  • Maintain provenance (full change history)

Current Progress & Challenges

Basic NFT implementation works (static traits)
Dynamic behavior not yet implemented
❌ Struggling with on-chain trait updates efficiently
❌ Need scalable storage for large collections

Key Questions

1. Architecture & Mutability

  • What’s the best architecture for mutable NFT traits?
    • Store traits in a separate mutable struct?
    • Use dynamic_field or dynamic_object_field?
  • How to preserve change history (provenance)?

2. Gas Efficiency

  • How to optimize storage for frequent trait updates?
  • Best patterns for batch upgrades (e.g., mass evolution events)?

3. Cross-NFT Interactions

  • How to implement merge/split logic safely?
  • Best way to handle shared resources (e.g., NFT composability)?

4. External Data Integration

  • Recommended patterns for oracle-triggered updates?
  • How to emit events for off-chain tracking?
  • Sui
  • NFT Ecosystem
  • Move
3
5
Share
Comments
.

Answers

5
obito.
Sep 7 2025, 17:22

For mutable NFT traits, dynamic_object_field is often best. It allows you to attach separate, evolvable trait objects directly to your main NFT, keeping the base NFT lean and updates modular. To preserve provenance, emit a detailed event with event::emit() for every significant trait change – this provides an immutable history for off-chain indexers without bloating your on-chain state. For gas efficiency, keep the core NFT object minimal. Batch upgrades by iterating through a vector of NFT IDs in a single transaction, ensuring it stays within gas limits. For merge/split, consider burning components and minting a new NFT, or using dynamic_object_field to attach/detach child NFTs, preserving their IDs. dynamic_object_field also supports composability by allowing NFTs to 'own' other NFTs as components. For oracle integration, use a 'pull' model: the oracle updates a well-known shared object, and your NFT's update function reads from it, triggering the change. Always emit events for off-chain tracking of these updates.

9
Comments
.
Benjamin XDV.
Jul 25 2025, 11:52

1. Core Dynamic NFT Architecture**

Dynamic Field Traits (Recommended)
module dynamic_nft::hero {  
    use sui::dynamic_field;  
    use sui::object::{Self, UID};  

    struct Hero has key {  
        id: UID,  
        // Immutable core  
        generation: u64,  
        // Mutable traits in dynamic fields  
    }  

    struct Strength has store { value: u8 }  
    struct Level has store { value: u32 }  

    public fun upgrade(  
        hero: &mut Hero,  
        amount: u8,  
        _admin: &AdminCap  
    ) {  
        let current: &mut Strength = dynamic_field::borrow_mut(&hero.id, Strength);  
        current.value = current.value + amount;  
    }  
}  

Why This Works:

  • Separates immutable identity from mutable state
  • Enables O(1) trait access
  • Supports arbitrary trait types

2. Advanced Patterns

Composable NFTs (Merge/Split)
public entry fn merge_heroes(  
    hero1: Hero,  
    hero2: Hero,  
    ctx: &mut TxContext  
): Hero {  
    let new_id = object::new(ctx);  
    // Transfer traits  
    dynamic_field::transfer(&hero1.id, &new_id, Strength);  
    dynamic_field::transfer(&hero2.id, &new_id, Agility);  
    // Burn originals  
    object::delete(hero1);  
    object::delete(hero2);  
    Hero { id: new_id, generation: next_gen() }  
}  
Oracle-Triggered Updates
struct WeatherOracle has key {  
    id: UID,  
    current_weather: u8 // 0=sunny, 1=rainy  
}  

public entry fn apply_weather_effects(  
    nft: &mut DynamicNFT,  
    oracle: &WeatherOracle  
) {  
    if (oracle.current_weather == 1) {  
        dynamic_field::add(&mut nft.id, b"effect", WeatherEffect::Rain);  
    }  
}  

3. Storage Optimization

Trait Packing
struct PackedTraits has store {  
    strength: u8,  
    agility: u8,  
    luck: u8,  
    // Uses 3 bytes instead of 3 dynamic fields  
}  
Batched Updates
public entry fn batch_upgrade(  
    heroes: vector<&mut Hero>,  
    amount: u8  
) {  
    let i = 0;  
    while (i < vector::length(&heroes)) {  
        upgrade(vector::borrow_mut(&mut heroes, i), amount);  
        i = i + 1;  
    }  
}  

4. Provenance & History

Immutable Event Log
struct NFTHistory has key {  
    id: UID,  
    events: vector<HistoryEvent>  
}  

struct HistoryEvent has store {  
    timestamp: u64,  
    changed_trait: vector<u8>,  
    old_value: vector<u8>,  
    new_value: vector<u8>  
}  

public fun record_change(  
    history: &mut NFTHistory,  
    event: HistoryEvent  
) {  
    vector::push_back(&mut history.events, event);  
}  
Versioned Traits
struct TraitV1 has store { value: u8 }  
struct TraitV2 has store {  
    base: u8,  
    multiplier: u8  
}  

public fun migrate_trait(v1: TraitV1): TraitV2 {  
    TraitV2 { base: v1.value, multiplier: 1 }  
}  

5. Testing Strategies

Trait Simulation
#[test]  
fun test_trait_evolution() {  
    let mut hero = create_hero();  
    upgrade(&mut hero, 5);  
    let strength: &Strength = dynamic_field::borrow(&hero.id, Strength);  
    assert!(strength.value == 5, EUpgradeFailed);  
}  
Fuzz Testing
#[test_only]  
fun fuzz_merge(  
    hero1: Hero,  
    hero2: Hero  
) {  
    let merged = merge_heroes(hero1, hero2);  
    assert!(dynamic_field::exists_with_type<Strength>(&merged.id), EMergeFailed);  
}  
7
Comments
.
Marlowe.
Sep 5 2025, 18:04

For a dynamic NFT collection on Sui, dynamic_field or dynamic_object_field are your best friends for mutable traits. Store trait structs directly as dynamic fields on the NFT object itself. This keeps the NFT's main structure clean and updates efficient, as you're only modifying the field, not the whole object. For provenance, emit an event::emit() every time a trait changes, including the old and new values along with the NFT ID. This creates an auditable history off-chain without bloating on-chain storage. Gas efficiency for updates is naturally handled by dynamic fields, as they only serialize the changed field. For batch upgrades, you'll likely need to process NFTs in chunks due to gas limits, but the dynamic field approach means each individual update is cheap. For merge/split, you'll need functions that transfer ownership, burn existing NFTs (if applicable), and mint new ones, or modify an existing NFT's fields. Shared resources or composability can be handled by NFTs holding IDs of other objects they interact with, or by them referencing shared objects. For oracle integration, your update functions should be permissioned to only allow calls from a trusted oracle's address or a specific capability object held by the oracle. Always event::emit() any significant state changes for easy off-chain tracking and indexing.

5
Comments
.

Do you know the answer?

Please log in and share it.