Post
Share your knowledge.
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
The Receivingticket
Think of Receiving
How do you get a Receiving
Important: A Receiving
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
- 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
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.