Sui Ownership: Transfer, Share, Freeze & Wrapping

This split lets a module enforce custom transfer rules: give a type key but not store, and only your module can move it (soulbound tokens, rule-enforced assets).

29.1 The transfer functions

transfer::transfer(obj, addr) Only the module defining T transfer::public_transfer(obj, addr) T: key + store

transfer::receive(parent_uid, ticket) / public_receive Receiving objects sent to an object Defining module / store types

This split lets a module enforce custom transfer rules: give a type key but not store, and only your module can move it (soulbound tokens, rule-enforced assets).

29.2 Examples

module demo::ownership;

public struct Badge has key { id: UID, level: u8 }           // soulbound: no store
public struct Gem has key, store { id: UID, carats: u64 }    // freely transferable
public struct Config has key { id: UID, fee_bps: u64 }

public fun award(to: address, ctx: &mut TxContext) {
    transfer::transfer(Badge { id: object::new(ctx), level: 1 }, to);
}

public fun mint_gem(carats: u64, ctx: &mut TxContext): Gem {
    Gem { id: object::new(ctx), carats }
}
// Callers (or PTBs) can do transfer::public_transfer(gem, someone)

public fun publish_config(ctx: &mut TxContext) {
    transfer::freeze_object(Config { id: object::new(ctx), fee_bps: 30 });
}

29.3 Shared objects

An object must be shared in the same transaction it is created; you cannot share an already-owned object.

Shared objects can be deleted (taken by value and unpacked) but not transferred or frozen afterwards.

public struct Board has key { id: UID, messages: vector<vector<u8>> }

public fun create_board(ctx: &mut TxContext) {
    transfer::share_object(Board { id: object::new(ctx), messages: vector[] });
}

public fun post(board: &mut Board, msg: vector<u8>) {
    board.messages.push_back(msg);
}

29.4 Wrapping

Putting an object inside another object's field wraps it: it disappears from global object lookup until unwrapped.

Wrapping requires the inner type to have store. Use wrapping for escrows, bundles, and equipment where the parent strictly controls access.

public struct Sword has key, store { id: UID, damage: u64 }

public struct Hero has key {
    id: UID,
    sword: Option<Sword>,      // wrapped object
}

public fun equip(hero: &mut Hero, sword: Sword) {
    hero.sword.fill(sword);
}

public fun unequip(hero: &mut Hero): Sword {
    hero.sword.extract()
}

29.5 Transfer to object & receiving

Objects can be sent to another object's ID (as an address). The parent's module later receives them:

This enables "object wallets" whose contents are controlled by contract logic.

use sui::transfer::Receiving;

public fun accept_payment(account: &mut Account, sent: Receiving<Coin<SUI>>): Coin<SUI> {
    transfer::public_receive(&mut account.id, sent)
}