Sui.

Post

Share your knowledge.

Bounty+15

Xavier.eth.
Jun 17, 2025
Expert Q&A

How do ability constraints interact with dynamic fields in heterogeneous collections?

I'm building a marketplace that needs to handle multiple asset types with different ability requirements, and I've hit some fundamental questions about Move's type system. I want to store different asset types in the same collection, but they have different abilities:

  • Regular NFTs: key + store (transferable)
  • Soulbound tokens: key only (non-transferable)
  • Custom assets with transfer restrictions
public struct Marketplace has key {
    id: UID,
    listings: Bag,  // Want to store different asset types here
}

// This works for transferable assets
public fun list_transferable<T: key + store>(
    marketplace: &mut Marketplace,
    asset: T,
    price: u64
) { /* ... */ }

// But how to handle soulbound assets?
public fun list_soulbound<T: key>(  // No store ability
    marketplace: &mut Marketplace,
    asset_ref: &T,  // Can only take reference
    price: u64
) { /* How do I store metadata about this? */ }

Key Questions:

  • Ability Requirements: When using dynamic_field::add<K, V>(), does V always need store at compile time? Can wrapper types work around this?
  • Heterogeneous Storage: Can a single Bag store objects with different ability sets (key + store + copy vs key + store), and handle them differently at runtime?
  • Type Safety: Since dynamic fields perform type erasure, how do I maintain type safety when retrieving values? What's the pattern for storing type metadata?
  • Witness Pattern: How do ability constraints work with phantom types? Can I store Asset<Type1> and Asset<Type2> in the same collection and extract type info later?

Building a system where NFTs, soulbound tokens, and restricted assets all need marketplace functionality but with different transfer semantics.

I’ve tried wrapper types, multiple collections per ability set, separate type metadata storage. Each has tradeoffs between type safety, gas costs, and complexity.

  • Sui
  • Architecture
0
5
Share
Comments
.

Answers

5
harry phan.
Jun 18 2025, 03:58

You’re running into the core of Move’s type and ability system and you’re absolutely right to raise these questions when designing a heterogeneous marketplace.

First, about ability constraints: any value you store using dynamic_field::add<K, V>() must have the store ability at compile time. This is a hard rule in Move: values without store simply can’t be persisted in storage, whether in dynamic fields, Tables, or similar constructs. Trying to wrap a non-store type in a struct won’t work either—Move is strict here. If even one field lacks store, the entire wrapper loses store too. So unfortunately, there’s no wrapper workaround here. Soulbound assets with only key (and no store) are excluded from any collection that writes to global storage like a Bag or dynamic field. This is well documented in the Move abilities docs.

As for storing different asset types in the same collection: if they all implement key + store, then yes, a single Bag can store them, but under the hood, Move doesn’t do real polymorphism. The type must be known at access time, and the Bag must be homogeneous from the compiler’s perspective. This leads to your next question: how do we maintain type safety in dynamic fields? Since Move does type erasure for these collections, it won’t remember what exact type is inside—you need to provide the mapping. One common pattern is to wrap each asset in a struct that includes metadata like a u8 enum or a TypeName, so when you retrieve the object, you inspect the metadata and downcast or handle accordingly. There’s no magic RTTI; you fake it with data discipline.

Now about phantom types and witness patterns—these are helpful for compile-time safety but don’t give you runtime introspection. So yes, you can create Asset and have T vary, but every instantiation of Asset must still satisfy the collection’s ability constraints. If you want to store both Asset and Asset in the same place, they must both implement store, and SoulboundToken likely won’t. You’ll need to handle those differently—possibly using references or storing only metadata for non-transferable assets, instead of trying to store the object.

In your marketplace design, this means you’re likely looking at a hybrid strategy. For regular NFTs and other transferable assets, you can use dynamic object fields or Bags freely, as long as they conform to key + store. For soulbound assets, which are key only, your best bet is to store references and associated metadata in a parallel metadata Bag, or use a custom listing type that doesn’t store the asset itself but records its existence via ID and supplementary data. It’s more complex, but it gives you the flexibility to enforce transfer semantics properly.

References

  1. https://github.com/MystenLabs/sui/blob/c7ec9546978b3c52b0c57bbdb9693f5068dd3383/external-crates/move/documentation/book/src/abilities.md
  2. https://github.com/sui-foundation/sui-move-intro-course/blob/92bb4986ad91c5ffd01c10c5b0d3bbbfa9d12507/unit-four/lessons/2_dynamic_fields.md
1
Best Answer
Comments
.
Owen.
Owen486
Jun 23 2025, 14:33

1. Do values in dynamic fields always need store?

If you want to store the value directly in a dynamic field (e.g., via dynamic_field::add), it must satisfy all the abilities required by the field's type — including store. If V does not have store, you cannot put it into a dynamic field directly.

But here's the trick:

You can wrap such types inside another struct that has store, even if the inner type doesn't.

Example:

struct AssetWrapper<T: key> has store {
    inner: T,
}

Even though T only has key, AssetWrapper<T> can be stored because it itself has store.

Now you can do:

dynamic_field::add(&mut object, key, AssetWrapper { inner: soulbound_token });

This allows you to store both transferable and non-transferable assets under the same abstraction.


2. Can a single Bag store objects with different ability sets?

The short answer is: yes, using wrappers and type erasure techniques. But there are tradeoffs.

In Move, you can’t directly have a list of multiple concrete types unless they’re wrapped in a generic container.

A good approach is to use a wrapper like:

struct AnyAsset has store {
    // Maybe include metadata or witness type info
    data: Vector<u8>,     // serialized representation?
    type_info: TypeInfo, // Type metadata
}

Or better yet, use a variant-style enum encoding (if supported by your Move dialect):

enum AssetType has store {
    Transferable(NFT),
    Soulbound(SoulboundToken),
    Restricted(RestrictedAsset),
}

However, this requires exhaustive matching and doesn't scale well across modules.

So, again: wrapping + dynamic fields + type metadata is more flexible.


3. How to maintain type safety when retrieving?

Move performs type erasure in dynamic fields. So once you retrieve a value from a dynamic field, you just get a value of some type V — but you don't know what V is at runtime.

To solve this safely, you need to:

  • Store type metadata alongside the value.
  • Use a witness pattern or type tag embedding to reassert type information upon retrieval.

Pattern: Store type metadata

struct AssetWithMetadata<T: key> has store {
    value: T,
    type_info: TypeInfo,
}

Then store AssetWithMetadata<NFT> or AssetWithMetadata<SoulboundToken> in a dynamic field.

When you retrieve, check the type_info against the expected type before doing anything unsafe.


4. Witness Pattern & Phantom Types

You can absolutely use phantom types as witnesses to encode behavior, like transferability.

For example:

struct Transferable; // marker type
struct NonTransferable; // marker type

struct Asset<T: drop + copy + store + key, Policy: phantom> has key, store {
    id: UID,
    policy: PhantomData<Policy>,
}

Then:

type NFT = Asset<_, Transferable>;
type Soulbound = Asset<_, NonTransferable>;

You can then write logic like:

public fun transfer<T>(asset: Asset<T, Transferable>) {
    // allowed
}

public fun transfer<T>(_asset: Asset<T, NonTransferable>) {
    // disallowed
    abort();
}

This gives you compile-time enforcement of transfer rules.

But to store both types in the same collection, you’ll still need a wrapper:

struct AnyAsset has store {
    data: Vector<u8>,
    type_info: TypeInfo,
}

And handle deserialization dynamically based on type_info.

5
Comments
.
0xduckmove.
Jun 18 2025, 04:11

Here’s how you can design a Sui Move marketplace that supports both transferable (NFTs, key + store) and soulbound (key only) assets, while maintaining type safety and handling ability constraints.

Ability Requirements for Dynamic Fields

Yes, V must have store at compile time for dynamic_field::add<K, V>() and similar collections.

  • store is required for any value to be stored in global storage, including dynamic fields and Tables.
  • Wrapper types cannot work around this: If you wrap a type without store in a struct, the wrapper cannot have store either, because all fields must have store for the struct to have store (source).

Heterogeneous Storage in a Bag

  • Bag (or any dynamic field-based collection) can only store types that satisfy the required abilities.
    • For dynamic object fields, the value must have key + store.
    • For dynamic fields, the value must have store.
  • You cannot store a type with only key (e.g., a soulbound token) in a Bag that requires store.
    • This is a fundamental restriction of the Move type system (source).

Type Safety and Type Erasure

  • Dynamic fields are type-erased at runtime. You must know the type you want to retrieve at access time.
  • Pattern for type safety:
    • Store type metadata (e.g., an enum or TypeName) alongside the value.
    • Use a wrapper struct that includes both the asset and its type info.
    • When retrieving, check the metadata before casting.
  • Example pattern:
    • Store Listing { asset_type: u8, ... } and use asset_type to determine how to handle the asset.

Witness Pattern and Phantom Types

  • Phantom types and the witness pattern help at compile time, not runtime.
    • You can use Asset for different types, but you still need to ensure all T have the required abilities for the collection.
    • At runtime, you cannot extract type info from a phantom type; you must store explicit metadata (source).
  1. Marketplace Design for Mixed Asset Types
  • Transferable assets (NFTs): Use key + store, store in dynamic object fields or Bags.

  • Soulbound tokens (key only): Cannot be stored in dynamic fields or Bags that require store. You must handle them separately, e.g., by storing only metadata or references (not the object itself).

  • Custom assets with restrictions: As long as they have key + store, you can store them. If not, you need a separate handling path.

  • All values in dynamic fields or Bags must have store (and key for dynamic object fields).

  • You cannot store types with only key in these collections.

  • Type safety at runtime requires explicit metadata.

  • Phantom types/witness pattern help at compile time, not runtime.

2
Comments
.
md rifat hossen.
Jun 19 2025, 17:11

Thank you Xavier.eth for this thoughtful and technical question. Here's my contribution regarding ability constraints and dynamic fields in Sui Move:

Yes, any value (V) used in dynamic_field::add<K, V>() must have store ability. This is strictly enforced in Move’s compile-time checks. Even wrapping a non-store value inside a struct won't help—if any field lacks store, the whole struct loses it too.

Storing heterogeneous assets with different abilities is possible, but only under the constraint that all assets in the dynamic field/Bags must satisfy the same ability set (typically key + store).

🔒 Soulbound assets with only key ability can't be stored directly in such collections. Instead, you can:

  • Store only metadata (ID, type enum, etc.)
  • Use a parallel mapping like dynamic_field::add<AssetID, Metadata> for soulbound types.
  • Keep a reference-based registry that tracks soulbound asset listings without storing the full object.

🧩 Phantom types help at compile time only. You’ll still need explicit runtime type metadata for actual retrieval logic.

🚀 Suggested hybrid architecture:

  • Bag<AssetID, TransferableAsset> for assets with store
  • Map<AssetID, SoulboundMetadata> for key-only types
  • AssetWrapper struct with type_id or u8 asset_type for identifying type at runtime

Thanks again! I look forward to seeing this marketplace evolve!

— md rifat hossen

2
Comments
.
24p30p.
Jul 9 2025, 05:17

If you're building a marketplace that supports multiple asset types with different abilities in Sui Move, the key challenge is how Move's ability constraints interact with dynamic fields and heterogeneous data. You can't directly store values without the store ability, so yes—when you use dynamic_field::add<K, V>(), the value V must have the store ability at compile time. This means you can’t directly store something like a soulbound token unless it’s wrapped in a type that has store. To work around this, you can use wrapper structs that hold metadata about the asset or a reference ID while satisfying the required abilities. For example, a SoulboundListing<T: key> wrapper can contain just a reference or metadata, not the full object. For heterogeneous storage, a Bag or Table can only store one type per instantiation, but you can make it work by wrapping assets in an enum-like custom type or using trait-like phantom types to simulate type information. You won't get native polymorphism, but you can track asset types with manual tagging or use the witness pattern, where phantom types and runtime discriminants help you simulate type-level separation. If you're using Asset<T> for each asset type, and both T1 and T2 are phantom, then yes—you can store Asset<T1> and Asset<T2> in a shared container by type-erasing to a common wrapper. You'll then need to store type info separately (like a type_id field) to correctly match when reading. It’s a balancing act between type safety, gas efficiency, and design complexity. Using separate collections per asset type is the cleanest for safety, but wrapper types and indexed metadata are more scalable when you want flexibility. You’ll need to choose based on whether gas cost or developer simplicity matters more for your use case.

0
Comments
.

Do you know the answer?

Please log in and share it.

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

434Posts646Answers
Sui.X.Peera.

Earn Your Share of 1000 Sui

Gain Reputation Points & Get Rewards for Helping the Sui Community Grow.

Reward CampaignJuly