DEV Community

Cover image for Understanding Objects in Cedra Move: The Complete Beginner's Guide (Part 1/3)
Daniel
Daniel

Posted on

Understanding Objects in Cedra Move: The Complete Beginner's Guide (Part 1/3)

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,
    });
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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...",
        ...
    });
}
Enter fullscreen mode Exit fullscreen mode

=> Named Objects (Deterministic Address)

let constructor_ref = object::create_named_object(
    creator, b"my_unique_seed"
);
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

Pro Tips: Use descriptive seeds with versioning

b"treasury_v1"
b"config_mainnet_v2" 
b"player_registry_2024"
Enter fullscreen mode Exit fullscreen mode

=> Sticky Objects (Non transferable)

let constructor_ref = object::create_sticky_object(owner_address);
Enter fullscreen mode Exit fullscreen mode

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(),
    });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • You need the object_signer to move resources to the object's address

  • The object_signer can 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)
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)