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)
}