llms.txt
Skip to main content

Storage Functions

The module that defines the main storage operations is sui::transfer. It is implicitly imported in all packages that depend on the Sui Framework, so, like other implicitly imported modules (e.g. std::option or std::vector), it does not require a use statement.

For quick reference, Appendix C: Transfer Functions contains a list of all storage functions and object states.

Overview

The transfer module provides a function for each of the ownership states an object can be placed into:

  1. Transfer - send an object to an address, putting it into the address owned state;
  2. Freeze - put an object into the immutable state, making it a public constant that can never change;
  3. Share - put an object into the shared state, available to everyone.

The transfer module is the go-to for most storage operations. Two special cases are covered separately: Dynamic Fields - attaching data to objects - in the next chapter, and receiving objects sent to other objects at the end of this one.

Ownership and References: a Quick Recap

Storage functions build directly on the semantics from the Ownership and Scope and References sections. All of them take the object by value: the object is moved into the function, the caller loses it - and, as we are about to see, it ends up in storage, in its new state. This is the resource model at work: an object is never copied into storage, it is placed there, and the previous owner provably gives it up. A function that only needs to read or update an object, on the other hand, takes it by reference (&T or &mut T) and leaves the ownership state untouched.

Internal Rule in Transfer Functions

Each storage operation comes in two forms: internal and public. The internal functions - transfer, share_object, freeze_object - enforce the internal constraint from the previous section: they can only be called in the module that defines the type of the object. The public versions - prefixed with public_ - lift that restriction, but require the type to have store in addition to key:

/// Internal: can only be called in the module that defines `T`.
public fun transfer<T: key>(obj: T, recipient: address);

/// Public: callable from any module, but requires `T` to have `store`.
public fun public_transfer<T: key + store>(obj: T, recipient: address);

Together, the two forms implement the rule we previewed in the store ability section: storage of a key-only object is fully governed by its defining module, while store opens the object up to storage operations performed by any module - and by the owner directly, in a transaction.

To see every combination at once, suppose module book::transfer_a defines two objects - ObjectK with key and ObjectKS with key + store - and module book::transfer_b tries to transfer them:

/// Imports the `ObjectK` and `ObjectKS` types from `transfer_a` and attempts
/// to implement different `transfer` functions for them.
module book::transfer_b;

// The types are not internal to this module!
use book::transfer_a::{ObjectK, ObjectKS};

// Fails! `ObjectK` is not internal to this module.
public fun transfer_k(k: ObjectK, to: address) {
transfer::transfer(k, to);
}

// Fails! `ObjectKS` is not internal to this module either -
// `store` does not affect the internal functions.
public fun transfer_ks(ks: ObjectKS, to: address) {
transfer::transfer(ks, to);
}

// Fails! `public_transfer` requires `store`, and `ObjectK` does not have it.
public fun public_transfer_k(k: ObjectK, to: address) {
transfer::public_transfer(k, to);
}

// Works! `ObjectKS` has `store`, and the function is public.
public fun public_transfer_ks(ks: ObjectKS, to: address) {
transfer::public_transfer(ks, to);
}

The same matrix applies to share_object/public_share_object and freeze_object/public_freeze_object. Knowing this rule is critical for understanding application design in Move: the choice between making an object publicly transferable (key + store) and keeping it internal (key-only) drastically affects the guarantees the application can give about its assets.

Transfer

The transfer::transfer function sends an object to an address, making that address its sole owner:

module sui::transfer;

// Transfer `obj` to `recipient`.
public fun transfer<T: key>(obj: T, recipient: address);

// Public version of the `transfer` function.
public fun public_transfer<T: key + store>(obj: T, recipient: address);

In the following example, a module creates an object representing the application's admin rights and sends it to the publisher of the module:

/// A struct with `key` is an object. The first field is `id: UID`!
public struct AdminCap has key { id: UID }

/// `init` is a special function called once, when the module is
/// published. It is the best place to create singleton objects,
/// such as an admin capability.
fun init(ctx: &mut TxContext) {
// Create the `AdminCap` object in this scope.
let admin_cap = AdminCap { id: object::new(ctx) };

// Transfer the object to the transaction sender.
transfer::transfer(admin_cap, ctx.sender());
}

/// Transfers the `AdminCap` object to the `recipient`. Thus, the
/// recipient becomes the owner of the object, and only they can
/// access it.
public fun transfer_admin_cap(cap: AdminCap, recipient: address) {
transfer::transfer(cap, recipient);
}

When the module is published, the init function is called, and the AdminCap object created in it is transferred to the transaction sender - ctx.sender() returns the sender address of the current transaction. (The init function is covered in detail in the Module Initializer section.)

From that point, say the sender was 0xa11ce, the object is in the address owned state: only 0xa11ce can use it in a transaction - by reference or by value, including transferring it further with the transfer_admin_cap function above.

Address-owned objects are subject to true ownership - only the owner address can access them. This is a fundamental concept in the Sui storage model, introduced in the Ownership section.

Public Transfer

Let's extend the example with a function that uses the AdminCap to authorize minting of a new object and transferring it to any address:

/// Some `Gift` object that the admin can `mint_and_transfer`.
public struct Gift has key, store { id: UID }

/// Creates a new `Gift` object and transfers it to the `recipient`.
public fun mint_and_transfer(
_: &AdminCap,
recipient: address,
ctx: &mut TxContext,
) {
let gift = Gift { id: object::new(ctx) };
transfer::public_transfer(gift, recipient);
}

The mint_and_transfer function "could" be called by anyone - it is public - but it requires an AdminCap reference as its first argument, and the AdminCap object is owned by 0xa11ce exclusively. So in practice only 0xa11ce can mint. This simple and explicit way of gating access to a function is the Capability pattern, one of the cornerstones of Sui application design.

Note the difference between the two objects in this example. AdminCap is key-only: the module keeps full control over it, and if the module exposed no transfer_admin_cap function, the admin rights would be soulbound - impossible to give away. Gift has key + store: it is sent with public_transfer, and whoever owns a Gift can freely transfer it onward in their own transactions, without any help from this module.

Quick Recap

  • transfer sends an object to an address, making it address owned;
  • Only the owner can use an address-owned object - by reference or by value;
  • Requiring a key-only object as an argument gates a function to the object's owner - the Capability pattern;
  • public_transfer is the public form: callable anywhere, requires key + store.

Freeze

The transfer::freeze_object function puts an object into the immutable state. Once an object is frozen, it can never change, and anyone can access it by immutable reference:

module sui::transfer;

// Make the object immutable and allow anyone to read it.
public fun freeze_object<T: key>(obj: T);

// Public version of the `freeze_object` function.
public fun public_freeze_object<T: key + store>(obj: T);

Let's extend the running example with a Config object that the admin creates and freezes:

/// Some `Config` object that the admin can `create_and_freeze`.
public struct Config has key {
id: UID,
message: String,
}

/// Creates a new `Config` object and freezes it.
public fun create_and_freeze(
_: &AdminCap,
message: String,
ctx: &mut TxContext,
) {
let config = Config {
id: object::new(ctx),
message,
};

// Freeze the object so it becomes immutable.
transfer::freeze_object(config);
}

/// Returns the message from the `Config` object.
/// Can access the object by immutable reference!
public fun message(c: &Config): String { c.message }

Once create_and_freeze is called, the Config becomes publicly available by its ID, and the message function can be called by anyone - on a frozen object, immutable references are free for the taking.

Function definitions are not tied to the object's state, so it is perfectly legal to define functions that take a frozen type by mutable reference or by value - they just cannot be called with a frozen object:

/// The function can be defined, but it won't be callable on a frozen
/// object - only immutable references to it are available.
public fun message_mut(c: &mut Config): &mut String { &mut c.message }

The same applies to delete_config, defined below in the Share section: it takes Config by value, and a frozen Config can never be passed to it. Freezing is permanent: a frozen object cannot be modified, transferred, deleted - or unfrozen.

Owned → Frozen

Since the freeze_object signature accepts any object by value, it can receive an object created in the same scope, but also an object the sender owns. Single Owner → Immutable conversion is possible! For example, an owner of a Gift can decide to preserve it forever:

/// Freezes the `Gift` object so it becomes immutable.
/// `Gift` has `key` + `store`, so `public_freeze_object` can be used!
public fun freeze_gift(gift: Gift) {
transfer::public_freeze_object(gift);
}

For obvious security reasons, this is also something to keep in mind in the other direction: an AdminCap must never be frozen - a frozen capability would be readable by everyone, and every function gated by &AdminCap would become callable by anyone. Which, once again, shows the value of the key-only pattern: AdminCap has no store, so external code has no way to freeze it, and the module simply does not expose a freezing function.

Quick Recap

  • freeze_object puts an object into the immutable state - permanently;
  • A frozen object is readable by anyone via immutable reference, and can never be modified, transferred, or deleted;
  • Owned objects can be frozen - including by their owner in a transaction, if the object has store;
  • public_freeze_object is the public form: callable anywhere, requires key + store.

Share

The transfer::share_object function puts an object into the shared state, where anyone can access it by mutable (and hence also immutable) reference:

module sui::transfer;

/// Put the object into the shared state - accessible to everyone.
public fun share_object<T: key>(obj: T);

/// Public version of the `share_object` function.
public fun public_share_object<T: key + store>(obj: T);
/// Creates a new `Config` object and shares it.
public fun create_and_share(message: String, ctx: &mut TxContext) {
let config = Config {
id: object::new(ctx),
message,
};

// Share the object so it becomes shared.
transfer::share_object(config);
}

Unlike freeze_object, which accepts both new and owned objects, share_object has a runtime restriction: only an object created in the same transaction can be shared. An attempt to share an object that already exists in the owned state aborts with ESharedNonNewObject. There is no Owned → Shared conversion: the decision to make an object shared has to be made at its creation. And like freezing, sharing is one-way - once shared, an object stays shared for the rest of its life, with a single exception, which we look at next.

Special Case: Shared Object Deletion

While a shared object can't normally be taken by value, there is one special case where it can - if the function that takes it deletes it. This is a special case in the Sui storage model, made to allow cleaning up shared state. Let's add a function that deletes the shared Config:

/// Deletes the `Config` object, takes it by value.
/// Can be called on a shared object!
public fun delete_config(c: Config) {
let Config { id, message: _ } = c;
id.delete()
}

The delete_config function takes the Config by value and destroys it completely - unpacking the struct and deleting the UID - and the Sui Verifier allows this call. However, if the function returned the Config, or attempted to transfer or freeze it, the transaction would be rejected:

// Won't work!
public fun transfer_shared(c: Config, to: address) {
transfer::transfer(c, to);
}

The rule: a shared object taken by value must be deleted in the same transaction.

Quick Recap

  • share_object puts an object into the shared state, accessible to everyone by mutable reference;
  • Only an object created in the same transaction can be shared - there is no Owned → Shared conversion;
  • Sharing is permanent, with one exception: a shared object may be taken by value in order to be deleted;
  • public_share_object is the public form: callable anywhere, requires key + store.

Party Transfer

The transfer module also provides party_transfer and public_party_transfer, which place an object into the party state - single-owner access with consensus ordering. Party objects are an advanced, newer feature, and we leave them out of the running example; the function signatures are listed in Appendix C, and the details are covered in the sui::party module documentation.

Summary

FunctionResulting stateReversible?Public version
transferAddress ownedYes - transfer awaypublic_transfer
freeze_objectImmutableNopublic_freeze_object
share_objectSharedOnly by deletionpublic_share_object
party_transferPartyDepends on permissionspublic_party_transfer
  • Every storage function takes the object by value - placing an object into storage consumes it;
  • Internal versions require the type to be defined in the calling module; public_* versions require store instead.

Next Steps

Now that you know the main features of the transfer module, you can start building applications that involve storage operations. In the next section we cover the UID and ID types - the identity of every object - and after that, Receiving as Object, the mechanism behind objects owning other objects.

Further Reading

llms.txt