If you're new to Cedra (Aptos) Move and Objects confuse you, you're absolutely not alone.
Coming from Solidity, Rust, or any traditional smart-contract model, the Object system in Move can feel like an entirely different universe.
This three-part series breaks everything down step-by-step — what Objects are, why they exist, and how to use them without the confusion. Cedra Move and Aptos Move work the same way, so we’ll use both terms interchangeably.
Let’s jump straight into today’s topic: why Objects exist and what problem they solve.
Why Objects Even Exist
First, I'll attempt to define what an Object is in Move:
Objects are containers that hold your data and live at their own address, separate from your account.
In basic Move, when you create a struct, it must live inside someone's account as a resource:
struct GameItem has key {
name: vector<u8>,
power: u64,
}
// this GameItem MUST live in an account
public entry fun create_item(owner: &signer) {
move_to(owner, GameItem {
name: b"Sword",
power: 100,
});
}
This creates several limitations:
Resources are glued to accounts → hard to transfer ownership
Only one account can own it → no shared ownership
Either you own it or you don’t → no complex permissions
It's hard to compose -> Can't build on top of others' resources
Inflexible Storage -> Stuck in the original account forever
The Solution: Objects
Objects are smart containers that:
Live at their own address (not in user accounts)
Have built-in ownership tracking
Can be transferred like assets
Support complex permission models
Allow modular composition
Where Objects Actually Live
In my opinion, this is the number 1 source of confusion for beginners.
Common Misconception
Objects are stored in the creator's account address
This is FALSE!
The Truth
When you create an Object:
It gets its own unique address in global storage
This address is either deterministic (based on seed) or random
It exists independently in the blockchain
Your account just gets metadata about ownership
What's Actually Stored Where
In the Object's address (0xabc...):
=> ObjectCore - ownership info, transfer rules, GUID
=> Your custom structs/resources
=> Any capabilities (refs) you store there (We'll explore refs better in subsequent parts).
In your account (0x123):
=> Reference/proof that you own it
=> NOT the actual data
Types of Objects
=> Normal Objects (Random Address)
let constructor_ref = object::create_object(owner_address);
Characteristics:
- Gets a random address
- Fully transferable by default
- It is deleteable
- Can be modified if you have refs
- Most common type
Example use cases:
- NFTs
- Gaming items
- Tradeable assets
- Assets that should change owners
Example:
public entry fun mint_nft(creator: &signer) {
let creator_addr = signer::address_of(creator);
// create transferable object
let constructor_ref = object::create_object(creator_addr);
let object_signer = object::generate_signer(&constructor_ref);
// add your NFT data
move_to(&object_signer, NFTData {
name: b"Cool Dragon",
image_uri: b"ipfs://Qm...",
...
});
}
=> Named Objects (Deterministic Address)
let constructor_ref = object::create_named_object(
creator, b"my_unique_seed"
);
Characteristics:
- Address is derived from
creator_address + seed - Same creator + seed = same address (predictable)
- Can only be created once per seed
- Not deleteable
- Transferable by default
Use cases:
- Protocol-level resources (treasury, registry)
- Singleton patterns
- Global leaderboards
- Configuration objects
- Resources you need to find without storing the address
Example - Game leaderboard:
const NAME: vector<u8> = b"global_leaderboard_v1";
const E_NOT_AUTHORIZED: u64 = 1;
//the Init Function
public entry fun init_leaderboard(admin: &signer) {
let admin_addr = signer::address_of(admin);
// strictly enforce that only the deployer can call this
assert!(admin_addr == @my_module, E_NOT_AUTHORIZED);
// create the object
let constructor_ref = object::create_named_object(
admin, NAME
);
// generate the signer so we can add resources to this object
let object_signer = object::generate_signer(&constructor_ref);
move_to(&object_signer, Leaderboard {
top_players: vector::empty(),
season: 1,
});
}
// later, anyone can find it without storing the address
#[view]
public fun get_leaderboard(): address {
let admin_addr = @my_module;
object::create_object_address(&admin_addr, NAME)
}
Pro Tips: Use descriptive seeds with versioning
b"treasury_v1"
b"config_mainnet_v2"
b"player_registry_2024"
=> Sticky Objects (Non transferable)
let constructor_ref = object::create_sticky_object(owner_address);
Characteristics:
- PERMANENTLY bound to the owner
- Cannot be transferred under ANY circumstances
- Owner cannot even transfer it themselves
- Perfect for identity/reputation
- Not deletable
- Has random address
Use cases:
- Soulbound tokens (SBTs)
- Achievement badges
- Certifications/credentials
- Identity tokens
- Reputation scores
- KYC/AML credentials
Example - Achievement system
public entry fun award_achievement(
player: address,
achievement_id: u64
) {
// this achievement is permanently bound to the player
let constructor_ref = object::create_sticky_object(player);
let object_signer = object::generate_signer(&constructor_ref);
move_to(&object_signer, Achievement {
id: achievement_id,
name: b"Dragon Slayer",
earned_at: timestamp::now_seconds(),
});
}
When NOT to use:
- Regular NFTs (use regular objects)
- Anything that might need to be traded
- Items that should be transferable "just in case"
Quick Decision Tree
Need predictable address?
└─→ Named Object
Should NEVER transfer?
└─→ Sticky Object
Everything else (NFTs, items, etc.)?
└─→ Normal Object
How to Add Data to Objects
public entry fun create_game_item(creator: &signer) {
// step 1: create the object
let constructor_ref = object::create_object(
signer::address_of(creator)
);
// step 2: get a signer for the object's address
let object_signer = object::generate_signer(&constructor_ref);
// step 3: move your struct INTO the object's address
move_to(&object_signer, GameItem {
name: b"legendary Sword",
attack: 150,
durability: 100,
});
// this is optional
// Store the object's address for later
let object_addr = object::address_from_constructor_ref(&constructor_ref);
}
Key points:
You need the
object_signerto move resources to the object's addressThe
object_signercan only be created during object creation (via ConstructorRef)Your data lives at the object's address, not your account
Accessing Objects Later
Reading Object Data
public fun get_item_stats(object_addr: address): (vector<u8>, u64)
acquires GameItem
{
let item = borrow_global<GameItem>(object_addr);
(item.name, item.attack)
}
Modifying Object Data
public entry fun upgrade_item(object_addr: address)
acquires GameItem
{
// check ownership first
// ... ownership validation ...
let item = borrow_global_mut<GameItem>(object_addr);
item.attack = item.attack + 10;
}
Transferring Ownership
public entry fun transfer_item(
owner: &signer,
object_addr: address,
recipient: address
) {
// verify owner has permission
let owner_addr = signer::address_of(owner);
assert!(object::owner(object_addr) == owner_addr, E_NOT_OWNER);
// transfer
object::transfer(owner, object_addr, recipient);
}
What's Next?
Now that you understand what Objects are and where they live, you're ready for the real power: Refs (Capabilities).
In Part 2, we'll cover:
- What are Refs and why they matter
- Types of Refs explained
- How to generate and store Refs
- When to use each Ref type
Quick Reference Card
// Normal Object (transferable)
object::create_object(owner_addr)
// Named Object (predictable address)
object::create_named_object(creator, seed)
// Sticky Object (non-transferable)
object::create_sticky_object(owner_addr)
Found this helpful? Drop a like and stay tuned for Part 2 where we explore Refs.
Questions? Drop them in the comments below.
You can also reach me on Twitter now X.
Top comments (0)