Post

Share your knowledge.

Post

Share your knowledge.

article banner.
harry phan.
Apr 13, 2025
Article

Object Access Control & receive Mechanics

This is the part 2 of "Parent-Child Objects in Sui Move" series. You can read part 1 here

Object Access Control transfer::receive Mechanics

So you’ve put an object X inside a parent P (by transferring X to P’s ID) – how do you get it back out or use it? 🤔 That’s where Sui’s special receive mechanism comes in.

When an object is transferred to a parent, it doesn’t automatically pop out. It sits there, owned by P. To use or remove that child object in a transaction, you must receive it. Sui provides a structured way to do this using a Receiving ticket and the transfer::receive function .

The  Receivingticket

Think of Receiving as a claim ticket for a child object of type T. In a transaction, instead of directly passing the child object (which you don’t own yet) to a function, you pass a Receiving – essentially a reference to “the object with ID Y that is owned by parent X”. This Receiving is a special Move struct that carries the ID, version, and digest of the object to be received . It has only the drop ability, meaning it can exist ephemerally but can’t be stored persistently  . In other words, it’s a one-time use ticket inside a transaction.

How do you get a Receiving? Usually, by performing the transfer. In a programmable transaction block (PTB), one step can transfer object C to parent P, which yields a Receiving ticket that the next step can use . If the child was already sitting in the parent from an earlier transaction, you can supply a Receiving as an input to a new transaction (the Sui SDK/CLI allows you to specify a child object by its ID and parent to generate the ticket).

Important: A Receiving is not the object itself – it doesn’t give you ownership yet. It just signals that “object of type T with this ID is owned by that parent, and I intend to take it.” To actually grab the object, you must call transfer::receive:


module example::toy_box {
    use sui::transfer::{Self, Receiving};

    struct Toy has key { id: UID, name: vector<u8> }
    struct Box has key { id: UID }

    /// Remove a Toy child from a Box, returning the Toy to the caller.
    public entry fun take_toy(box: &mut Box, toy_ticket: Receiving<Toy>): Toy {
        // Use the parent's UID and the Receiving ticket to get the Toy
        let toy = transfer::receive(&mut box.id, toy_ticket);
        // Now `toy` is an owned value we can return (or transfer to someone).
        toy
    }
}

In take_toy, the toy_ticket: Receiving represents a Toy that belongs to the box. By calling transfer::receive(&mut box.id, toy_ticket), we invoke Sui’s native logic to actually retrieve the Toy object out from the box . This does a few critical things:

  • It verifies at runtime that the toy_ticket indeed references an object currently owned by the box (using the parent’s UID) . If something doesn’t match (wrong parent, or object not actually there), it will abort.
  • It then returns the actual Toy object as an owned value in the transaction, meaning now the Toy is in our function’s control.

Notice we had to pass &mut box.id. Sui enforces that we have a mutable reference to the parent’s UID to call receive .

This is a clever access control: only the module that can produce a &mut UID of the parent can allow receiving. Typically, the parent type’s defining module will expose a function like take_toy that internally calls receive. If the parent module doesn’t expose any function that gives out a &mut UID (directly or indirectly), then no external code can snatch its children. This way, the parent’s module controls how and when children can be accessed .

In the example, Box has an id: UID field. Since take_toy is in the same module, it can borrow &mut box.id. External modules or transactions can call take_toy(box_obj, ticket) but they cannot themselves call transfer::receive on Box because box.id is private.

This pattern ensures only authorized code can retrieve children.

What about nested objects? If Toy was literally a field inside Box (say Box { id, toy: Toy }), we wouldn’t need any receive – the toy would be accessible whenever you have &mut Box. But that also means removing it or treating it separately is harder (it’s like part of the box). With child objects, we decouple storage: the toy is separate and must be explicitly taken out. This explicitness is why Sui requires the Receiving ticket and receive call – it makes child retrieval an authorized action.

Mutable reference requirement: You might wonder, why &mut UID and not just &UID? Mutable ensures the parent is locked during the receive operation, preventing concurrent modification and ensuring the caller actually has the right to modify that parent (usually implying they are the owner). It’s part of Sui’s dynamic ownership checks – by taking a mutable borrow of the parent’s UID, Sui guarantees no other transaction or parallel action can interfere while you’re pulling out the child. It’s a bit like locking the parent’s door before removing the contents.

Example usage in a transaction block: Suppose a Box object is owned by Alice, and she has a Toy object she wants to put into the box and then perhap s take it out later. Here’s how it could loo k:

  • Attach toy to box: Alice calls transfer::public_transfer(toy, @); in a PTB. This transfers the toy under the box (we use public_transfer here because it’s a transaction context – more on that soon). Now the toy is a child of the box.
  • In the same or another transaction, retrieve toy: Alice invokes example::toy_box::take_toy(box_obj, <Receiving>) where that Receiving refers to the toy. Under the hood, take_toy calls receive, verifies the relationship, and returns the Toy. Now the Toy would be an output of the transaction, typically ending up back under Alice’s address (since we returned it from an entry function).

Key Takeaways:

  • Child objects aren’t accessible by default; you need a Receiving ticket and an appropriate function to get them out.
  • The parent’s module decides how you can retrieve (by providing a function with &mut UID).
  • transfer::receive is used within the parent’s defining module for objects of that module or its friends. If the child’s type is defined elsewhere, you’ll need a different approach (enter public_receive…).

Before moving on, one more detail: if an object type T has only the key ability (no store), Sui treats it as a bit more restricted. Such objects cannot be received by generic code outside their module . In practice, this means if T is key-only and becomes a child, you’ll need to handle its retrieval in its own module or the parent’s module with a custom rule. If T also has store, we have more flexibility via public_receive. Let’s explore that next.

  • Sui
2
Share
Comments
.

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

213Posts315Answers
Sui.X.Peera.

Earn Your Share of 1000 Sui

Gain Reputation Points & Get Rewards for Helping the Sui Community Grow.

We use cookies to ensure you get the best experience on our website.
More info