Dynamic Fields

Sui Object model allows objects to be attached to other objects as dynamic fields. The behavior is similar to how a Map works in other programming languages. However, unlike a Map which in Move would be strictly typed (we have covered it in the Collections section), dynamic fields allow attaching objects of any type. A similar approach from the world of frontend development would be a JavaScript Object type which allows storing any type of data dynamically.

There's no limit to the number of dynamic fields that can be attached to an object. Thus, dynamic fields can be used to store large amounts of data that don't fit into the object limit size.

Dynamic Fields allow for a wide range of applications, from splitting data into smaller parts to avoid object size limit to attaching objects as a part of application logic.

Definition

Dynamic Fields are defined in the sui::dynamic_field module of the Sui Framework. They are attached to object's UID via a name, and can be accessed using that name. There can be only one field with a given name attached to an object.

File: sui-framework/sources/dynamic_field.move

/// Internal object used for storing the field and value
public struct Field<Name: copy + drop + store, Value: store> has key {
    /// Determined by the hash of the object ID, the field name
    /// value and it's type, i.e. hash(parent.id || name || Name)
    id: UID,
    /// The value for the name of this field
    name: Name,
    /// The value bound to this field
    value: Value,
}

As the definition shows, dynamic fields are stored in an internal Field object, which has the UID generated in a deterministic way based on the object ID, the field name, and the field type. The Field object contains the field name and the value bound to it. The constraints on the Name and Value type parameters define the abilities that the key and value must have.

Usage

The methods available for dynamic fields are straightforward: a field can be added with add, removed with remove, and read with borrow and borrow_mut. Additionally, the exists_ method can be used to check if a field exists (for stricter checks with type, there is an exists_with_type method).

module book::dynamic_collection {
    // a very common alias for `dynamic_field` is `df` since the
    // module name is quite long
    use sui::dynamic_field as df;
    use std::string::String;

    /// The object that we will attach dynamic fields to.
    public struct Character has key {
        id: UID
    }

    // List of different accessories that can be attached to a character.
    // They must have the `store` ability.
    public struct Hat has key, store { id: UID, color: u32 }
    public struct Mustache has key, store { id: UID }

    #[test]
    fun test_character_and_accessories() {
        let ctx = &mut tx_context::dummy();
        let mut character = Character { id: object::new(ctx) };

        // Attach a hat to the character's UID
        df::add(
            &mut character.id,
            b"hat_key",
            Hat { id: object::new(ctx), color: 0xFF0000 }
        );

        // Similarly, attach a mustache to the character's UID
        df::add(
            &mut character.id,
            b"mustache_key",
            Mustache { id: object::new(ctx) }
        );

        // Check that the hat and mustache are attached to the character
        //
        assert!(df::exists_(&character.id, b"hat_key"), 0);
        assert!(df::exists_(&character.id, b"mustache_key"), 1);

        // Modify the color of the hat
        let hat: &mut Hat = df::borrow_mut(&mut character.id, b"hat_key");
        hat.color = 0x00FF00;

        // Remove the hat and mustache from the character
        let hat: Hat = df::remove(&mut character.id, b"hat_key");
        let mustache: Mustache = df::remove(&mut character.id, b"mustache_key");

        // Check that the hat and mustache are no longer attached to the character
        assert!(!df::exists_(&character.id, b"hat_key"), 0);
        assert!(!df::exists_(&character.id, b"mustache_key"), 1);

        sui::test_utils::destroy(character);
        sui::test_utils::destroy(mustache);
        sui::test_utils::destroy(hat);
    }
}

In the example above, we define a Character object and two different types of accessories that could never be put together in a vector. However, dynamic fields allow us to store them together in a single object. Both objects are attached to the Character via a vector<u8> (bytestring literal), and can be accessed using their respective keys.

As you can see, when we attached the accessories to the Character, we passed them by value. In other words, both values were moved to a new scope, and their ownership was transferred to the Character object. If we changed the ownership of Character object, the accessories would have been moved with it.

And the last important property of dynamic fields we should highlight is that they are accessed through their parent. This means that the Hat and Mustache objects are not directly accessible and follow the same rules as the parent object.

Foreign Types as Dynamic Fields

Dynamic fields allow objects to carry data of any type, including those defined in other modules. This is possible due to their generic nature and relatively weak constraints on the type parameters. Let's illustrate this by attaching a few different values to a Character object.

let mut character = Character { id: object::new(ctx) };

// Attach a `String` via a `vector<u8>` name
df::add(&mut character.id, b"string_key", b"Hello, World!".to_string());

// Attach a `u64` via a `u32` name
df::add(&mut character.id, 1000u32, 1_000_000_000u64);

// Attach a `bool` via a `bool` name
df::add(&mut character.id, true, false);

In this example we showed how different types can be used for both name and the value of a dynamic field. The String is attached via a vector<u8> name, the u64 is attached via a u32 name, and the bool is attached via a bool name. Anything is possible with dynamic fields!

Orphaned Dynamic Fields

To prevent orphaned dynamic fields, please, use Dynamic Collection Types such as Bag as they track the dynamic fields and won't allow unpacking if there are attached fields.

The object::delete() function, which is used to delete a UID, does not track the dynamic fields, and cannot prevent dynamic fields from becoming orphaned. Once the parent UID is deleted, the dynamic fields are not automatically deleted, and they become orphaned. This means that the dynamic fields are still stored in the blockchain, but they will never become accessible again.

let hat = Hat { id: object::new(ctx), color: 0xFF0000 };
let mut character = Character { id: object::new(ctx) };

// Attach a `Hat` via a `vector<u8>` name
df::add(&mut character.id, b"hat_key", hat);

// ! DO NOT do this in your code
// ! Danger - deleting the parent object
let Character { id } = character;
id.delete();

// ...`Hat` is now stuck in a limbo, it will never be accessible again

Orphaned objects are not a subject to storage rebate, and the storage fees will remain unclaimed. One way to avoid orphaned dynamic fields during unpacking on an object is to return the UID and store it somewhere temporarily until the dynamic fields are removed and handled properly.

Custom Type as a Field Name

In the examples above, we used primitive types as field names since they have the required set of abilities. But dynamic fields get even more interesting when we use custom types as field names. This allows for a more structured way of storing data, and also allows for protecting the field names from being accessed by other modules.

/// A custom type with fields in it.
public struct AccessoryKey has copy, drop, store { name: String }

/// An empty key, can be attached only once.
public struct MetadataKey has copy, drop, store {}

Two field names that we defined above are AccessoryKey and MetadataKey. The AccessoryKey has a String field in it, hence it can be used multiple times with different name values. The MetadataKey is an empty key, and can be attached only once.

let mut character = Character { id: object::new(ctx) };

// Attaching via an `AccessoryKey { name: b"hat" }`
df::add(
    &mut character.id,
    AccessoryKey { name: b"hat".to_string() },
    Hat { id: object::new(ctx), color: 0xFF0000 }
);
// Attaching via an `AccessoryKey { name: b"mustache" }`
df::add(
    &mut character.id,
    AccessoryKey { name: b"mustache".to_string() },
    Mustache { id: object::new(ctx) }
);

// Attaching via a `MetadataKey`
df::add(&mut character.id, MetadataKey {}, 42);

As you can see, custom types do work as field names but as long as they can be constructed by the module, in other words - if they are internal to the module and defined in it. This limitation on struct packing can open up new ways in the design of the application.

This approach is used in the Object Capability pattern, where an application can authorize a foreign object to perform operations in it while not exposing the capabilities to other modules.

Exposing UID

Mutable access to UID is a security risk. Exposing UID of your type as a mutable reference can lead to unwanted modifications or removal of the object's dynamic fields. Additionally, it affects the Transfer to Object and Dynamic Object Fields. Make sure to understand the implications before exposing the UID as a mutable reference.

Because dynamic fields are attached to UIDs, their usage in other modules depends on whether the UID can be accessed. By default struct visibility protects the id field and won't let other modules access it directly. However, if there's a public accessor method that returns a reference to UID, dynamic fields can be read in other modules.

/// Exposes the UID of the character, so that other modules can read
/// dynamic fields.
public fun uid(c: &Character): &UID {
    &c.id
}

In the example above, we show how to expose the UID of a Character object. This solution may work for some applications, however, it is important to remember that exposed UID allows reading any dynamic field attached to the object.

If you need to expose the UID only within the package, use a restrictive visibility, like public(package), or even better - use more specific accessor methods that would allow only reading specific fields.

/// Only allow modules in the same package to access the UID.
public(package) fun uid_package(c: &Character): &UID {
    &c.id
}

/// Allow borrowing dynamic fields from the character.
public fun borrow<Name: copy + store + drop, Value: store>(
    c: &Character,
    n: Name
): &Value {
    df::borrow(&c.id, n)
}

Dynamic Fields vs Fields

Dynamic Fields are more expensive than regular fields, as they require additional storage and costs for accessing them. Their flexibility comes at a price, and it is important to understand the implications when making a decision between using dynamic fields and regular fields.

Limits

Dynamic Fields are not subject to the object size limit, and can be used to store large amounts of data. However, they are still subject to the dynamic fields created limit, which is set to 1000 fields per transaction.

Applications

Dynamic Fields can play a crucial role in applications of any complexity. They open up a variety of different use cases, from storing heterogeneous data to attaching objects as part of the application logic. They allow for certain upgradeability practices based on the ability to define them later and change the type of the field.

Next Steps

In the next section we will cover Dynamic Object Fields and explain how they differ from dynamic fields, and what are the implications of using them.