Sui.

Post

Share your knowledge.

LOLLYPOP.
Sep 19, 2025
Expert Q&A

Gas Optimization in Dynamic NFT Metadata Updates

How can I optimize gas usage in a Sui smart contract when batching Move transactions that modify shared objects, especially when handling dynamic NFT metadata updates?

  • Sui
  • Architecture
  • SDKs and Developer Tools
  • NFT Ecosystem
  • Move
0
2
Share
Comments
.

Answers

2
draSUIla.
Sep 19 2025, 16:18

I’ve had to carefully consider gas optimization—especially when batching transactions that modify shared objects like dynamic NFTs.

Gas costs can escalate quickly in Sui when multiple shared objects are mutated in one transaction. Here's how I typically optimize gas usage when handling batched updates to dynamic NFT metadata.


🧠 Core Optimization Strategies

  1. Minimize Object Mutations: Only mutate what’s strictly necessary.
  2. Use Event Emission for Logs: Instead of storing a full history on-chain.
  3. Batch Using a Controller Object: Use a single shared object as a controller to mutate other child objects.
  4. Avoid Repeated Borrowing of Shared Objects: This adds gas due to dependency resolution.
  5. Use option<T> and defaulting over vector pushes: More gas-efficient.

💡 Use Case: Batched Metadata Update for Dynamic NFTs

Let’s say I want to update metadata (like levels, scores, traits) on multiple NFTs owned by a user. Here’s how I design it:


🧱 1. Shared Object: MetadataController

This acts as a manager for metadata updates. Only it gets mutated in batched calls.

module example::metadata_controller {

    use sui::object::UID;
    use sui::tx_context::TxContext;
    use sui::event;
    use sui::option::{Self, Option};

    struct Metadata has key {
        level: u64,
        score: u64,
        name: vector<u8>,
    }

    struct MetadataController has key {
        id: UID,
        // Mapping NFT ID to Metadata
        metadata_store: table::Table<address, Metadata>,
    }

    public fun create(ctx: &mut TxContext): MetadataController {
        MetadataController {
            id: object::new(ctx),
            metadata_store: table::new<address, Metadata>(ctx),
        }
    }

    public fun batch_update(
        controller: &mut MetadataController,
        nft_ids: vector<address>,
        levels: vector<u64>,
        scores: vector<u64>,
        names: vector<vector<u8>>,
    ) {
        let len = vector::length(&nft_ids);
        assert!(
            len == vector::length(&levels)
            && len == vector::length(&scores)
            && len == vector::length(&names),
            0
        );

        let i = 0;
        while (i < len) {
            let nft = *vector::borrow(&nft_ids, i);
            let metadata = Metadata {
                level: *vector::borrow(&levels, i),
                score: *vector::borrow(&scores, i),
                name: vector::borrow(&names, i),
            };
            table::insert(&mut controller.metadata_store, nft, metadata);
            i = i + 1;
        }

        event::emit(ctx, BatchUpdated {
            updated_count: len as u64
        });
    }

    struct BatchUpdated has copy, drop, store {
        updated_count: u64,
    }

}

🛠️ 2. Client-Side Batching

On the frontend or CLI, I prepare the batched call once, so Sui can resolve dependencies efficiently:

// pseudo-code using Sui JS SDK
const tx = new TransactionBlock();

tx.moveCall({
  target: "example::metadata_controller::batch_update",
  arguments: [
    tx.object(controllerId), // shared object
    tx.pure(nftAddresses),   // vector<address>
    tx.pure(levels),         // vector<u64>
    tx.pure(scores),         // vector<u64>
    tx.pure(names),          // vector<vector<u8>>
  ],
});

let result = await suiProvider.signAndExecuteTransactionBlock({
  transactionBlock: tx,
  signer: wallet,
});

📉 Why This Is Gas-Efficient

  • Only one shared object (controller) is being mutated.
  • All NFT metadata is stored in a central table, avoiding multiple object references.
  • You avoid expensive &mut access to each NFT shared object individually.
  • Using vectors and loop logic in Move is cheaper than multiple separate calls.

🧪 Optional: Off-Chain Indexing

Since the metadata is stored under a controller, indexers (like Suiet or custom infra) can extract and associate it off-chain to user-facing NFTs. This saves gas and keeps the user experience rich.


✅ Summary Checklist

OptimizationApplied
Single shared object mutation
Batch update with vectors
Avoided redundant borrows
Used table for key-value
Event used instead of logs

If you're working with on-chain metadata updates for a large NFT set (e.g., in-game characters, evolving collectibles), this pattern works really well. Let me know if you want a version where each NFT is a separate shared object—there are tricks for that too, but it gets more expensive.

0
Comments
.
Turnerlee69.
Oct 7 2025, 23:02

To reduce gas costs when batching Move transactions that update shared objects like dynamic NFT metadata in a Sui smart contract, you should minimize unnecessary state changes and structure your logic to avoid repeated reads and writes. One key approach is to group metadata updates efficiently—only touch fields that actually change and avoid full object rewrites. You can also use event-driven patterns or off-chain triggers to determine when updates are needed, instead of updating on every interaction. When batching, combine multiple metadata updates into a single transaction block where possible, which reduces overhead per operation. Also, leverage Sui’s object model smartly: design your NFT metadata as separate, modular objects that can be updated independently instead of embedding everything into one large object. Finally, reusing object references and optimizing access patterns in Move code (like borrowing instead of copying objects) will keep gas usage low during execution.

Read more: https://docs.sui.io/build/gas

0
Comments
.

Do you know the answer?

Please log in and share it.