Post
Share your knowledge.

Building a Marketplace with Heterogeneous Assets
When building a marketplace on a blockchain using the Move programming language, one of the most intriguing challenges is managing assets with different ability constraints in a single collection. Whether you're dealing with transferable NFTs, non-transferable soulbound tokens, or custom assets with unique transfer restrictions, Move's strict type system demands careful design to ensure type safety and efficiency. In this post, we'll dive into how ability constraints interact with dynamic fields in heterogeneous collections, explore practical solutions, and share a robust approach to building a marketplace that handles diverse asset types.
Understanding Move's Abilities
Move, designed for blockchains like Sui and Aptos, uses abilities to define what operations a type supports. The two key abilities relevant to our marketplace are:
- key: Allows a type to be stored in global storage as an object.
- store: Permits a type to be embedded within another object, such as a struct or collection.
Our marketplace needs to handle:
- Regular NFTs: Have key + store, making them transferable and storable.
- Soulbound Tokens: Have only key, meaning they’re non-transferable and cannot be stored in other objects.
- Custom Assets: Have varying abilities, potentially with transfer restrictions.
The goal is to store these assets in a single collection, like a Bag, and manage their listings while respecting Move's type system.
The Challenge: Dynamic Fields and Ability Constraints
Move's dynamic_field::add<K, V>()
function allows adding fields to objects dynamically, which seems ideal for a heterogeneous collection. However, the value type V must have the store ability. This poses a problem for soulbound tokens, which lack store. So, how do we store and manage assets with different ability sets in a marketplace?
Key Questions
- Does V in dynamic fields always need store? Can we use wrapper types to work around this?
- Can a single Bag store objects with different abilities (e.g., key + store vs. key)?
- How do we maintain type safety with dynamic fields' type erasure?
- How do phantom types and the witness pattern help manage heterogeneous assets?
Let’s tackle each question and build a solution.
Ability Requirements for Dynamic Fields
The Move documentation confirms that dynamic_field::add<K, V>() requires V to have the store ability. This is because dynamic fields are stored within an object, and Move enforces that embedded values must be storable. For regular NFTs with key + store, this is straightforward—we can store them directly in a Bag or dynamic field.
For soulbound tokens with only key, direct storage is impossible. A wrapper type, like struct Wrapper
struct Metadata has store {
id: ID,
price: u64,
asset_type: String,
}
Heterogeneous Storage in a Single Collection
A Bag in Move is designed to store values with the store ability, but all values must conform to the same type constraints. This means a single Bag cannot store both NFTs (key + store) and soulbound token metadata unless they’re wrapped in a common type with store. However, mixing types in one collection often leads to complexity and potential type safety issues.
A better approach is to use separate collections for different ability sets:
- Assets: Store the assets directly in a vector
or Bag. - Soulbound Tokens: Store their IDs and metadata in a vector
.
This separation respects Move’s ability constraints while keeping the system modular and maintainable.
Maintaining Type Safety
Dynamic fields erase type information at runtime, so retrieving a value requires specifying the type at compile time, like dynamic_field::remove<K, V>(). This ensures type safety but complicates handling heterogeneous types. To manage different asset types, store a type tag (e.g., a String like "NFT" or "SoulboundToken") in the metadata. At runtime, check the tag to determine how to process the listing.
For example:
public struct ListingMetadata has store {
asset_id: ID,
price: u64,
asset_type: String,
}
When retrieving, use the asset_type
to decide whether to treat the asset as an NFT or soulbound token, ensuring correct handling while maintaining compile-time type safety for the stored metadata.
Phantom Types and the Witness Pattern
Phantom types in Move, like struct Asset
To extract type info later, store a metadata field like asset_type alongside the asset or its ID. This allows you to differentiate Asset
A Practical Marketplace Design
Here’s a practical implementation for a marketplace that handles both transferable and soulbound assets:
Marketplace Structure
use sui::object::{Self, UID, ID};
use sui::vec_map::{Self, VecMap};
struct Marketplace has key {
id: UID,
transferable_listings: VecMap<ID, ListingWithAsset>,
soulbound_listings: VecMap<ID, ListingMetadata>,
}
struct ListingWithAsset has store {
asset: T, // T must have key + store
price: u64,
}
struct ListingMetadata has store {
asset_id: ID,
price: u64,
asset_type: String,
}
Transferable Assets:
public fun list_transferable<T: key + store>(
marketplace: &mut Marketplace,
asset: T,
price: u64
) {
let id = object::id(&asset);
let listing = ListingWithAsset { asset, price };
vec_map::insert(&mut marketplace.transferable_listings, id, listing);
}
Soulbound Tokens:
ublic fun list_soulbound<T: key>(
marketplace: &mut Marketplace,
asset: &T,
price: u64
) {
let id = object::id(asset);
let listing = ListingMetadata { asset_id: id, price, asset_type: "SoulboundToken" };
vec_map::insert(&mut marketplace.soulbound_listings, id, listing);
}
Building a marketplace in Move taught me the importance of aligning with the language’s type system. Abilities like store are non-negotiable for dynamic fields, so planning for metadata storage early is critical for non-storable assets. Separate collections simplify handling different ability sets, while metadata tags enable runtime flexibility. Phantom types are great for compile-time distinctions, but runtime type handling requires explicit metadata.
- Sui
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

- 24p30p... SUI+78
1
- MoonBags... SUI+71
2
- Meaning.Sui... SUI+43
3
- ... SUIJojo+34
- ... SUIOpiiii+31
- ... SUI0xduckmove+20
- ... SUIHaGiang+20
- ... SUIfomo on Sui+16
- ... SUI
- ... SUI
- Why does BCS require exact field order for deserialization when Move structs have named fields?53
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution43
- Sui Transaction Failing: Objects Reserved for Another Transaction25
- How do ability constraints interact with dynamic fields in heterogeneous collections?05