Skip to main content

Transfer to Object

You can transfer objects to an object ID in the same way you transfer objects to an address, using the same functions. This is because Sui does not distinguish between the 32-byte ID of an address and the 32-byte ID of an object (which are guaranteed not to overlap). The transfer to object operation takes advantage of this feature, allowing you to provide an object ID as the address input of a transfer operation.

Because of the identical ID structure, you can use an object ID for the address field when transferring an object. In fact, all functionality around address-owned objects works the same for objects owned by other objects, you just replace the address with the object ID.

When you transfer an object to another object, you're basically establishing a form of parent-child authentication relationship. Objects that you have transferred to another object can be received by the (possibly transitive) owner of the parent object. The module that defines the type of the parent (receiving) object also defines the access control for receiving a child object.

These restrictions for accessing sent child objects are enforced dynamically by providing mutable access to the parent object's UID during the execution of the transaction. Because of this, you can transfer objects to and receive them from owned objects, dynamic field objects, wrapped objects, and shared objects.

One of the benefits of the transfer to object operation is the ability to have a stable ID for an on-chain wallet or account, for example. The transfer of the object doesn't affect its ID, regardless of the state of the object that you send it to. When you transfer an object, all of that object's child objects move with it, and the object's address remains the same whether you transfer it, wrap it, or hold it as a dynamic field.

Transferring to object

Just like with normal object transfers, you must make sure that the object ID exists that you are transferring the object to. Additionally, make sure that the object that you are transferring to is not immutable. You can't access an object transferred to an immutable object.

You should know the type of the object you are transferring to because the module that defines that object can:

  • Define predicates that can be dynamically checked to access the sent object.
  • Lack support for accessing objects that have been sent to it. Future versions of that package might support this functionality, but it's up to the package author to include it.
// 0xADD is an address
// 0x0B is an object ID
// b and c are objects

// Transfers the object `b` to the address 0xADD
sui::transfer::public_transfer(b, @0xADD);

// Transfers the object `c` to the object with object ID 0x0B
sui::transfer::public_transfer(c, @0x0B);

Transferring an object to an object ID results in the same result as if you transferred the object to an address - the object's owner is the 32-byte address or object ID provided. Additionally, because there is no difference in the result of the object transfer, you can use existing RPC methods such as getOwnedObjects on the 32-byte ID. If the ID represents an address, then the method returns the objects owned by that address. If the ID is an object ID, then the method returns the objects the object ID owns (transferred objects).

// Get the objects owned by the address 0xADD. Returns `b`.
{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getOwnedObjects",
"params": [
"0xADD"
]
}

// Get the objects owned by the object with object ID 0x0B. Returns `c`
{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getOwnedObjects",
"params": [
"0x0B"
]
}

Receiving objects

After an object c has been sent to another object p, p must then receive c to do anything with it. To receive the object c, a Receiving(o: ObjectRef) argument type for programmable transaction blocks (PTBs) is used that takes an object reference containing the to-be-received object's ObjectID, Version, and Digest (just as owned object arguments for PTBs do). However, Receiving PTB arguments are not passed as an owned value or mutable reference within the transaction.

To explain further, look at the core of the receiving interface in Move, which is defined in the sui::transfer module in the Sui framework:

module sui::transfer {
/// Represents the ability to receive an object of type `T`. Cannot be stored.
struct Receiving<phantom T: key> has drop { ... }

/// Given mutable (i.e., locked) access to the `parent` and a `Receiving`
/// object referencing an object owned by `parent` use the `Receiving` ticket
/// and return the corresponding object.
/// NB: &mut UID here allows the defining module of the parent type to
/// define custom access/permission policies around receiving objects sent
/// to objects of a type that it defines. You can see this more in the examples.
public native fun receive<T: key>(parent: &mut UID, object: Receiving<T>): T;

...
}

Each Receiving argument referring to a sent object of type T in a PTB results in exactly one argument with a Move type of sui::transfer::Receiving<T>. You can then use this argument to receive the sent object of type T with the sui::transfer::receive function.

When you call the sui::transfer::receive function, you must pass a mutable reference to the parent object's UID. You can't get a mutable reference to the UID of an object, though, unless the defining module of the object exposes it. Consequently, the module that defines the type of the parent object that is receiving the child object defines access control policies and other restrictions on receiving objects that are sent to it. See the authorization example for a demonstration of this pattern. The fact that the passed-in UID actually owns the object referenced by the Receiving parameter is dynamically checked and enforced. This allows access to objects that have been sent to, for example, dynamic fields where the ownership chain can only be established dynamically.

Because sui::transfer::Receiving has only the drop ability, the existence of a Receiving<T> argument represents the ability, but not the obligation to receive the object of type T specified by the object reference in the PTB Receiving argument during that transaction. You can use some, none, or all Receiving arguments in a PTB without issue. Any object that corresponds to a Receiving argument remains untouched (in particular, its object reference remain the same) unless it is received.

Examples

The following examples demonstrate receiving previously sent objects.

Receiving objects from shared objects

Generally, if you want to allow receiving sent objects from shared objects that are defined in the module, add dynamic authorization checks; otherwise, anyone could receive sent objects. In this example a shared object SharedObject holds a counter that anyone can increment, but only the address 0xB0B can receive objects from the shared object.

module examples::shared_object_auth {
use sui::transfer::{Self, Receiving};
use sui::object;
use sui::tx_context::{Self, TxContext};
const EAccessDenied: u64 = 0;
const AuthorizedReceiverAddr: address = @0xB0B;

struct SharedObject has key {
id: object::UID,
counter: u64,
}

public fun create(ctx: &mut TxContext) {
let s = SharedObject { id: object::new(ctx), };
transfer::share_object(s);
}

/// Anyone can increment the counter in the shared object.
public fun increment(obj: &mut SharedObject) {
obj.counter = obj.counter + 1;
}

/// Objects can only be received from the `SharedObject` by the
/// `AuthorizedReceiverAddr` otherwise the transaction aborts.
public fun receive_object<T: key>(obj: &mut SharedObject, sent: Receiving<T>): T {
assert!(tx_context::sender(ctx) == AuthorizedReceiverAddr, EAccessDenied);
transfer::receive(&mut obj.id, sent)
}
}

Receiving objects and adding them as dynamic fields

This example defines a basic account-type model where an Account object holds its coin balances in different dynamic fields. This Account is also transferable to a different address or object.

Importantly, the address that coins are to be sent with this Account object remains the same regardless of whether the Account object is transferred, wrapped (for example, in an escrow account), or moved into a dynamic field. In particular, there is a stable ID for a given Account object across the object's lifecycle, regardless of any ownership changes.

module examples::account {
use sui::transfer::{Self, Receiving};
use sui::coin::{Self, Coin};
use sui::object;
use sui::dynamic_field as df;
use sui::tx_context::TxContext;

const EBalanceDONE: u64 = 1;

/// Account object that `Coin`s can be sent to. Balances of different types
/// are held as dynamic fields indexed by the `Coin` type's `type_name`.
struct Account has key {
id: object::UID,
}

/// Dynamic field key representing a balance of a particular coin type.
struct AccountBalance<phantom T> has copy, drop, store { }

/// This function will receive a coin sent to the `Account` object and then
/// join it to the balance for each coin type.
/// Dynamic fields are used to index the balances by their coin type.
public fun accept_payment<T>(account: &mut Account, sent: Receiving<Coin<T>>) {
// Receive the coin that was sent to the `account` object
let coin = transfer::receive(&mut account.id, sent);
let account_balance_type = AccountBalance<T>{};
let account_uid = &mut account.id;

// Check if a balance of that coin type already exists.
// If it does then merge the coin we just received into it,
// otherwise create new balance.
if (df::exists_(account_uid, account_balance_type)) {
let balance: &mut Coin<T> = df::borrow_mut(account_uid, account_balance_type);
coin::join(balance, coin);
} else {
df::add(account_uid, account_balance_type, coin);
}
}

/// Withdraw `amount` of coins of type `T` from `account`.
public fun withdraw<T>(account: &mut Account, amount: u64, ctx: &mut TxContext): Coin<T> {
let account_balance_type = AccountBalance<T>{};
let account_uid = &mut account.id;
// Make sure what we are withdrawing exists
assert!(df::exists_(account_uid, account_balance_type), EBalanceDONE);
let balance: &mut Coin<T> = df::borrow_mut(account_uid, account_balance_type);
coin::split(balance, amount, ctx)
}

/// Can transfer this account to a different address
/// (e.g., to an object or address).
public fun transfer_account(account: Account, to: address, _ctx: &mut TxContext) {
// Perform some authorization checks here and if they pass then transfer the account
// ...
transfer::transfer(account, to);
}
}