Claude-starter aptos-object-model

Expert on Aptos Object Model - ObjectCore, Object<T> wrapper, constructor references, ExtendRef/DeleteRef/TransferRef capabilities, object ownership, named vs generated objects, composability, and migration from resource-only patterns. Triggers on keywords object model, objectcore, constructorref, extendref, deleteref, transferref, named object, object ownership, composable object

install
source · Clone the upstream repo
git clone https://github.com/raintree-technology/claude-starter
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/raintree-technology/claude-starter "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/aptos/object-model" ~/.claude/skills/raintree-technology-claude-starter-aptos-object-model-4ad70a && rm -rf "$T"
manifest: .claude/skills/aptos/object-model/skill.md
source content

Aptos Object Model Expert

Purpose

Provide expert guidance on the Aptos Object Model - a powerful abstraction that enables composable, transferable, and flexible on-chain assets. The Object Model is the foundation for modern Aptos development including Token V2 (Digital Assets), Fungible Assets, and custom composable resources.

When to Use

Auto-invoke when users mention:

  • Object Model - objects, ObjectCore, Object<T>, object-based design
  • Object Creation - ConstructorRef, named objects, object generation
  • Object Capabilities - ExtendRef, DeleteRef, TransferRef, LinearTransferRef
  • Object Patterns - composability, nesting, soul-bound, ownership
  • Migration - moving from resource-only to object-based architecture
  • Standards - Digital Assets (NFTs), Fungible Assets built on objects

Core Concepts

What is the Aptos Object Model?

The Object Model is Aptos's object-oriented programming abstraction that:

  1. Wraps resources - Any resource can become an "object"
  2. Enables transfer - Objects can be transferred between accounts
  3. Provides composability - Objects can own other objects
  4. Manages lifecycle - Creation, extension, deletion via refs
  5. Separates ownership - Owner address ≠ object address

Key Difference from Traditional Resources

// Traditional Resource (can't be transferred directly)
struct MyResource has key {
    value: u64
}
// Lives at account address, can't move to another account

// Object-wrapped Resource (transferable)
struct MyResource has key {
    value: u64
}
// Lives at object address, object itself can transfer between accounts

Object Architecture

ObjectCore - The Foundation

Every object contains an

ObjectCore
at its address:

struct ObjectCore has key {
    guid_creation_num: u64,      // For generating unique IDs
    owner: address,              // Who owns this object
    allow_ungated_transfer: bool, // Can anyone transfer it?
    transfer_events: EventHandle<TransferEvent>,
}

Key Points:

  • ObjectCore
    is automatically created during object creation
  • Object's address ≠ owner's address (objects live at their own address)
  • Owner can be an account OR another object (composition!)
  • Transfer permissions controlled via
    allow_ungated_transfer

Object<T> - The Type Wrapper

struct Object<phantom T> has copy, drop, store {
    inner: address  // The object's address
}

Important:

  • Object<T>
    is a typed reference to an object
  • It's NOT the object itself, just a pointer
  • Has
    copy + drop + store
    (can be copied, stored anywhere)
  • Type parameter
    T
    indicates what resource is at that address
  • Phantom type parameter (zero runtime cost)

Type Hierarchy Example

// An NFT is an Object wrapping a Token resource
Object<Token>

// A Fungible Asset metadata object
Object<Metadata>

// Generic object
Object<ObjectCore>  // Can reference any object

Object Creation

Method 1: Named Objects (Deterministic Address)

use aptos_framework::object;

public fun create_named_object(creator: &signer) {
    let constructor_ref = object::create_named_object(
        creator,
        b"MY_UNIQUE_SEED"  // Seed for deterministic address
    );

    // Object address is deterministic: hash(creator_address, seed)
    let object_signer = object::generate_signer(&constructor_ref);
    let object_addr = signer::address_of(&object_signer);

    // Move resources to object address
    move_to(&object_signer, MyResource { value: 100 });
}

Named Object Features:

  • Deterministic address:
    hash(creator_address, seed)
  • Same creator + seed = same address (idempotent)
  • Useful for singletons, registries, well-known objects
  • Can't create duplicate with same creator + seed

Method 2: Generated Objects (Random Address)

public fun create_generated_object(creator: &signer) {
    let constructor_ref = object::create_object(creator);

    // Object address is generated (non-deterministic)
    let object_signer = object::generate_signer(&constructor_ref);

    move_to(&object_signer, MyResource { value: 100 });
}

Generated Object Features:

  • Non-deterministic address (GUID-based)
  • Each call creates new unique object
  • Useful for collections (NFTs, tokens, etc.)

Method 3: Object from Account

public entry fun create_object_from_account(caller: &signer) {
    // Convert account into an object
    let constructor_ref = object::create_object_from_account(caller);
    // Now the account itself becomes an object
}

Method 4: Sticky Object (Cannot Delete)

public fun create_sticky_object(creator: &signer) {
    let constructor_ref = object::create_sticky_object(creator);
    // No DeleteRef can be generated - object is permanent
}

Object References and Capabilities

ConstructorRef - The Master Key

struct ConstructorRef {
    self: address,
    can_delete: bool
}

Only available during object creation. Used to generate all other refs:

public fun create_with_refs(creator: &signer) {
    let constructor_ref = object::create_object(creator);

    // Generate various capabilities
    let extend_ref = object::generate_extend_ref(&constructor_ref);
    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    let delete_ref = object::generate_delete_ref(&constructor_ref);
    let mutator_ref = property_map::generate_mutator_ref(&constructor_ref);

    // Store refs for later use
    let object_signer = object::generate_signer(&constructor_ref);
    move_to(&object_signer, Refs {
        extend_ref,
        transfer_ref,
        delete_ref,
        mutator_ref,
    });
}

struct Refs has key {
    extend_ref: ExtendRef,
    transfer_ref: TransferRef,
    delete_ref: DeleteRef,
    mutator_ref: property_map::MutatorRef,
}

ExtendRef - Access Object After Creation

struct ExtendRef has drop, store {
    self: address
}

Purpose: Get signer of object in future transactions

public fun modify_object_later(
    object_addr: address
) acquires Refs {
    let refs = borrow_global<Refs>(object_addr);

    // Generate signer from ExtendRef
    let object_signer = object::generate_signer_for_extending(&refs.extend_ref);

    // Now can modify resources at object address
    let resource = borrow_global_mut<MyResource>(object_addr);
    resource.value = resource.value + 10;
}

TransferRef - Control Object Transfers

struct TransferRef has drop, store {
    self: address
}

Purpose: Enable/disable transfers, force transfers

// Disable ungated transfers (make soul-bound)
public fun make_soul_bound(object_addr: address) acquires Refs {
    let refs = borrow_global<Refs>(object_addr);
    object::disable_ungated_transfer(&refs.transfer_ref);
}

// Re-enable ungated transfers
public fun make_transferable(object_addr: address) acquires Refs {
    let refs = borrow_global<Refs>(object_addr);
    object::enable_ungated_transfer(&refs.transfer_ref);
}

// Force transfer (bypass ownership check)
public fun admin_transfer(
    object_addr: address,
    new_owner: address
) acquires Refs {
    let refs = borrow_global<Refs>(object_addr);
    object::transfer_with_ref(&refs.transfer_ref, new_owner);
}

LinearTransferRef - One-Time Transfer

struct LinearTransferRef {
    self: address,
    owner: address
}

Purpose: Transfer object exactly once (consumed on use)

public fun create_and_transfer_to(
    creator: &signer,
    recipient: address
) {
    let constructor_ref = object::create_object(creator);

    // Generate linear transfer ref
    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    let linear_transfer_ref = object::generate_linear_transfer_ref(&transfer_ref);

    // Transfer to recipient (consumes linear_transfer_ref)
    object::transfer_with_ref(linear_transfer_ref, recipient);
}

DeleteRef - Destroy Objects

struct DeleteRef has drop, store {
    self: address
}

Purpose: Delete object and all resources at its address

public fun delete_object(object_addr: address) acquires Refs, MyResource {
    // Remove all resources first
    let MyResource { value: _ } = move_from<MyResource>(object_addr);
    let Refs { delete_ref, extend_ref, transfer_ref, mutator_ref } =
        move_from<Refs>(object_addr);

    // Delete the object
    object::delete(delete_ref);
}

Object Ownership and Transfer

Reading Object Information

use aptos_framework::object;

public fun get_object_info<T: key>(obj: Object<T>): (address, address, bool) {
    let object_addr = object::object_address(&obj);
    let owner_addr = object::owner(obj);
    let is_owner_account = object::is_owner(obj, owner_addr);

    (object_addr, owner_addr, is_owner_account)
}

// Check if object exists
public fun object_exists<T: key>(addr: address): bool {
    object::object_exists<T>(addr)
}

User-Initiated Transfer

use aptos_framework::object;

public entry fun transfer_object<T: key>(
    owner: &signer,
    object: Object<T>,
    new_owner: address
) {
    // Only works if ungated transfer is enabled
    object::transfer(owner, object, new_owner);
}

Checking Transfer Permissions

public fun check_transferable<T: key>(obj: Object<T>): bool {
    object::ungated_transfer_allowed(obj)
}

Object Composition (Nesting)

Child Object Pattern

public fun create_parent_and_child(creator: &signer) {
    // Create parent object
    let parent_ref = object::create_named_object(creator, b"PARENT");
    let parent_signer = object::generate_signer(&parent_ref);
    let parent_addr = signer::address_of(&parent_signer);

    // Create child object OWNED BY parent
    let child_ref = object::create_object_from_object(&parent_signer);
    let child_signer = object::generate_signer(&child_ref);
    let child_addr = signer::address_of(&child_signer);

    // Transfer child to parent
    let transfer_ref = object::generate_transfer_ref(&child_ref);
    let linear_transfer_ref = object::generate_linear_transfer_ref(&transfer_ref);
    object::transfer_with_ref(linear_transfer_ref, parent_addr);

    // Now: parent owns child, creator owns parent
}

Querying Nested Ownership

public fun get_root_owner<T: key>(obj: Object<T>): address {
    let mut current_addr = object::object_address(&obj);

    loop {
        if (!object::is_object(current_addr)) {
            // Reached an account (not an object) - this is root owner
            return current_addr;
        };

        let owner = object::owner(object::address_to_object<ObjectCore>(current_addr));
        if (owner == current_addr) {
            return current_addr; // Self-owned
        };
        current_addr = owner;
    }
}

Common Patterns

Pattern 1: Registry / Singleton

public fun get_or_create_registry(creator: &signer): address {
    let seed = b"MY_REGISTRY";
    let creator_addr = signer::address_of(creator);
    let object_addr = object::create_object_address(&creator_addr, seed);

    if (!object::object_exists<Registry>(object_addr)) {
        let constructor_ref = object::create_named_object(creator, seed);
        let object_signer = object::generate_signer(&constructor_ref);

        move_to(&object_signer, Registry {
            items: simple_map::create()
        });
    };

    object_addr
}

Pattern 2: NFT with Composable Items

struct NFT has key {
    name: String,
    uri: String,
}

struct EquippedItem has key {
    item_object: Object<Item>,
}

struct Item has key {
    name: String,
    power: u64,
}

public fun equip_item_to_nft(
    nft_obj: Object<NFT>,
    item_obj: Object<Item>
) acquires Refs {
    let nft_addr = object::object_address(&nft_obj);

    // Get ExtendRef to modify NFT
    let refs = borrow_global<Refs>(nft_addr);
    let nft_signer = object::generate_signer_for_extending(&refs.extend_ref);

    // Store item reference in NFT
    if (!exists<EquippedItem>(nft_addr)) {
        move_to(&nft_signer, EquippedItem { item_object: item_obj });
    };

    // Transfer item ownership to NFT object
    let item_addr = object::object_address(&item_obj);
    let item_refs = borrow_global<Refs>(item_addr);
    object::transfer_with_ref(&item_refs.transfer_ref, nft_addr);
}

Pattern 3: Soul-Bound Token (SBT)

public fun create_soul_bound_token(
    creator: &signer,
    recipient: address,
    name: String
) {
    let constructor_ref = token::create_named_token(
        creator,
        string::utf8(b"Soul Bound Collection"),
        string::utf8(b"Description"),
        name,
        option::none(),
        string::utf8(b"https://uri.com"),
    );

    // Disable transfers (soul-bound)
    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    object::disable_ungated_transfer(&transfer_ref);

    // Transfer to recipient (one-time transfer during creation)
    let linear_transfer_ref = object::generate_linear_transfer_ref(&transfer_ref);
    object::transfer_with_ref(linear_transfer_ref, recipient);

    // Don't store transfer_ref - now permanently non-transferable
}

Pattern 4: Upgradeable Module Data

struct ModuleData has key {
    version: u64,
    config: Config,
}

struct DataRefs has key {
    extend_ref: ExtendRef,
}

public fun initialize_module_data(admin: &signer) {
    let constructor_ref = object::create_named_object(admin, b"MODULE_DATA");
    let object_signer = object::generate_signer(&constructor_ref);

    move_to(&object_signer, ModuleData {
        version: 1,
        config: create_config(),
    });

    let extend_ref = object::generate_extend_ref(&constructor_ref);
    move_to(&object_signer, DataRefs { extend_ref });
}

public fun upgrade_module_data(new_config: Config) acquires DataRefs, ModuleData {
    let data_addr = object::create_object_address(&@my_module, b"MODULE_DATA");
    let refs = borrow_global<DataRefs>(data_addr);

    let object_signer = object::generate_signer_for_extending(&refs.extend_ref);
    let data = borrow_global_mut<ModuleData>(data_addr);

    data.version = data.version + 1;
    data.config = new_config;
}

Migration from Resource-Only to Object Model

Before (Resource-Only)

struct OldNFT has key {
    name: String,
    uri: String,
}

// Can't transfer between accounts
// Lives at owner's address
// No composition

After (Object Model)

struct NewNFT has key {
    name: String,
    uri: String,
}

public fun create_nft(creator: &signer, recipient: address) {
    let constructor_ref = object::create_object(creator);
    let object_signer = object::generate_signer(&constructor_ref);

    move_to(&object_signer, NewNFT {
        name: string::utf8(b"My NFT"),
        uri: string::utf8(b"https://..."),
    });

    // Transfer to recipient
    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    let linear_ref = object::generate_linear_transfer_ref(&transfer_ref);
    object::transfer_with_ref(linear_ref, recipient);
}

// ✅ Can transfer
// ✅ Lives at own address
// ✅ Composable

TypeScript SDK Integration

Creating Objects

import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";

const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));

// Call object creation function
const txn = await aptos.transaction.build.simple({
  sender: account.accountAddress,
  data: {
    function: "0x123::my_module::create_named_object",
    functionArguments: [],
  },
});

const response = await aptos.signAndSubmitTransaction({
  signer: account,
  transaction: txn,
});

Querying Objects

// Get object data
const objectData = await aptos.getAccountResource({
  accountAddress: "0xobject_address",
  resourceType: "0x1::object::ObjectCore"
});

console.log("Owner:", objectData.owner);
console.log("Transferable:", objectData.allow_ungated_transfer);

// Get custom resource at object address
const nftData = await aptos.getAccountResource({
  accountAddress: "0xobject_address",
  resourceType: "0x123::my_module::NFT"
});

Transferring Objects

const txn = await aptos.transaction.build.simple({
  sender: owner.accountAddress,
  data: {
    function: "0x1::object::transfer",
    typeArguments: ["0x123::my_module::NFT"],
    functionArguments: [
      objectAddress,
      recipientAddress,
    ],
  },
});

Best Practices

✅ Do

  • Store refs at object address - Keep ExtendRef, DeleteRef, etc. as resources at the object's own address
  • Use named objects for singletons - Registries, module configs, well-known objects
  • Use generated objects for collections - NFTs, tokens, user-specific objects
  • Disable ungated transfer for SBTs - Use soul-bound pattern for credentials
  • Document object relationships - Make ownership hierarchy clear
  • Clean up on deletion - Remove all resources before calling object::delete()

❌ Avoid

  • Don't lose ConstructorRef - Generate all needed refs during creation
  • Don't store refs elsewhere - Store at object address, not creator address
  • Don't forget to transfer initial ownership - Objects start owned by creator
  • Don't mix object and non-object patterns - Pick one architecture
  • Don't delete without cleanup - Remove resources first

Common Errors and Solutions

Error: "Object does not exist"

// Problem: Object wasn't created or wrong address
let obj = object::address_to_object<T>(wrong_address);

// Solution: Verify object exists first
assert!(object::object_exists<T>(address), ERROR_OBJECT_NOT_FOUND);
let obj = object::address_to_object<T>(address);

Error: "Ungated transfer not allowed"

// Problem: Trying to transfer soul-bound object
object::transfer(owner, obj, recipient); // FAILS

// Solution: Use TransferRef for forced transfer
let refs = borrow_global<Refs>(object_addr);
object::transfer_with_ref(&refs.transfer_ref, recipient);

Error: "Object already exists"

// Problem: Creating named object with duplicate seed
let constructor_ref = object::create_named_object(creator, b"SEED");

// Solution: Check existence first or use different seed
let addr = object::create_object_address(&creator_addr, b"SEED");
if (!object::object_exists<ObjectCore>(addr)) {
    let constructor_ref = object::create_named_object(creator, b"SEED");
    // ...
}

Security Considerations

Access Control

// Verify ownership before operations
public fun modify_only_by_owner(
    owner: &signer,
    obj: Object<MyResource>
) {
    assert!(
        object::is_owner(obj, signer::address_of(owner)),
        ERROR_NOT_OWNER
    );

    // Safe to modify
}

Capability Protection

// Store refs at object address (not accessible from outside)
struct Refs has key {
    extend_ref: ExtendRef,
    transfer_ref: TransferRef,
    delete_ref: DeleteRef,
}

// ✅ Good: Stored at object address
move_to(&object_signer, Refs { /* ... */ });

// ❌ Bad: Stored at creator address (could be accessed elsewhere)
// move_to(creator, Refs { /* ... */ });

Documentation References

Reference official Aptos docs:

  • aptos.dev/guides/objects
  • aptos.dev/standards/digital-asset
  • aptos.dev/standards/fungible-asset
  • Aptos Framework source: 0x1::object

Response Style

  • Concept-first - Explain object model concepts before code
  • Pattern-driven - Show common patterns and use cases
  • Comparison - Compare to resource-only approach when helpful
  • Visual - Use diagrams/structure to show object relationships
  • Security-aware - Highlight ownership and access control

Follow-up Suggestions

After helping with objects, suggest:

  • Object composition strategies
  • Ref management patterns
  • Migration path from resources
  • Gas optimization for object operations
  • Testing object-based contracts
  • Integration with Token/FA standards