llms.txt
Skip to main content

UID and ID

The use of the UID type is required by the Sui Verifier on all types that have the key ability. Here we go deeper into UID and its usage.

Definition

The UID type is defined in the sui::object module and is a wrapper around an ID which, in turn, wraps the address type. The UIDs on Sui are guaranteed to be unique, and can't be reused after the object was deleted.

module sui::object;

/// UID is a unique identifier of an object.
public struct UID has store {
id: ID
}

/// ID is a wrapper around an address; freely copyable.
public struct ID has copy, drop, store {
bytes: address
}

Note the difference in abilities: an ID is plain, copyable data - a pointer that can name any object (or even a non-existent one) without any special privileges. A UID can be neither copied nor dropped: it is the identity of an object, and both its creation and its destruction are explicit, controlled operations.

Fresh UID Generation

A new UID is created with the object::new(ctx) function:

  • UID is derived from the transaction digest and a counter of IDs created so far in the transaction, which is incremented with each new UID.
  • The counter lives in the transaction context, which is why TxContext is required - as a mutable reference - for UID generation.
  • The id field of a newly created object must be a fresh UID - one produced by object::new in the same transaction. The Sui Verifier rejects packing an object with a UID taken from another, unpacked object - so an identity can never be reused, even by the module that owns it.

UID acts as the representation of an object, and enables features attached to the object's identity. One of the key ones - Dynamic Fields - is possible because the UID is explicit. Another - Transfer to Object, covered at the end of this chapter - allows an object to receive other objects sent to its ID.

UID Lifecycle

The UID is created with object::new, and deleted with the object::delete function. The delete function consumes the UID by value, so it can only be called after the object was unpacked - which, in turn, only the defining module can do:

public struct Character has key { id: UID }

/// Creates a `Character` object and immediately destroys it:
/// the UID can only be deleted after the object is unpacked.
public fun create_and_destroy(ctx: &mut TxContext) {
// Instantiate the `Character` object with a fresh UID.
let char = Character { id: object::new(ctx) };

// Unpack the object to take out its UID.
let Character { id } = char;

// Delete the UID.
id.delete();
}

Keeping the UID

The UID does not have to be deleted immediately after the object is unpacked. It may carry Dynamic Fields, or hold objects sent to it via Transfer to Object - deleting the UID would make those unreachable. For such cases, the UID can be kept: stored as a plain UID field (not as id!) in another struct, until the associated data is dealt with and the UID can be safely deleted.

The ability to keep a UID after its object is gone enables a niche technique known as proof of deletion: the returned UID is evidence that the object was destroyed, which an application can exchange for a reward, or use to bypass restrictions that applied to the live object.

UID Derivation

Sui allows deriving UIDs from other UIDs using derivation keys. This functionality is implemented in the sui::derived_object module, and produces predictable, deterministic IDs for easier offchain discovery. The UID for each parent + key pair can be claimed only once:

use sui::derived_object;

/// Some central application object.
public struct Base has key { id: UID }

/// A derived object.
public struct Derived has key { id: UID }

/// Creates and shares a new `Derived` object, using an `address`
/// as the derivation key.
public fun derive(base: &mut Base, key: address) {
let id = derived_object::claim(&mut base.id, key);
transfer::share_object(Derived { id })
}

Derived addresses reduce the load on offchain indexers: it is enough to know the ID of the parent object, and the IDs of derived objects can be computed with a derivation function - present in most SDKs, and in Move itself:

module sui::derived_object;

/// Checks if a UID was derived with `key` at `parent`.
public fun exists<K: copy + drop + store>(parent: &UID, key: K): bool;

/// Derive the inner `address` of a UID, regardless of whether it was claimed.
public fun derive_address<K: copy + drop + store>(parent: ID, key: K): address;

The same derivation mechanism is used internally to generate IDs for dynamic fields.

ID

When talking about UID we should also mention the ID type. It is a freely copyable wrapper around address, used to point at an object. Usually an ID refers to some object, but there is no restriction - and no guarantee - that the ID points to an existing object.

An ID can be received as a transaction argument in a Transaction Block. Alternatively, an ID can be created from an address value using the to_id() function.

public fun conversion_methods(ctx: &mut TxContext) {
let uid: UID = object::new(ctx);

// `to_inner` returns a copy of the underlying `ID`.
let id: ID = uid.to_inner();

// Both `UID` and `ID` can be converted to a plain address.
let addr_from_uid: address = uid.to_address();
let addr_from_id: address = id.to_address();

uid.delete();
}

Fresh Object Address

TxContext provides the fresh_object_address function, which produces a unique address using the same derivation as object::new - without creating a UID. It is useful for applications that need unique identifiers for offchain entities - for example, an order_id in a marketplace.

Summary

  • UID is the non-copyable, non-droppable identity of an object; ID is a freely copyable pointer.
  • Fresh UIDs come from object::new(ctx) and can never be reused for a new object.
  • A UID is deleted with object::delete after unpacking - or kept, if data is still attached to it.
  • Derived UIDs (sui::derived_object) make object IDs predictable and discoverable offchain.

Further Reading

llms.txt