home / skills / raintree-technology / claude-starter / object-model

object-model skill

/skills/aptos/object-model

This skill helps you model and manage transferable on-chain objects with ownership, composability, and lifecycle using ObjectCore, Object<T>, and refs.

npx playbooks add skill raintree-technology/claude-starter --skill object-model

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.6 KB
---
name: aptos-object-model
description: Expert on Aptos Object Model for composable, transferable assets. Covers ObjectCore, Object<T> wrapper, ConstructorRef, ExtendRef, DeleteRef, TransferRef capabilities, object ownership, named vs generated objects, and composability patterns.
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
model: sonnet
license: MIT
metadata:
  author: raintree
  version: "1.0"
---

# Aptos Object Model Expert

Expert on the Aptos Object Model for building composable, transferable on-chain assets.

## Triggers

- object model, objectcore, Object<T>
- constructorref, extendref, deleteref, transferref
- named object, generated object
- object ownership, composable object
- soul-bound, nesting

## Core Concepts

The Object Model enables:
- **Transferable resources** - Objects can move between accounts
- **Composability** - Objects can own other objects
- **Lifecycle management** - Create, extend, delete via refs
- **Ownership separation** - Owner address != object address

## ObjectCore

Every object has an `ObjectCore`:

```move
struct ObjectCore has key {
    owner: address,
    allow_ungated_transfer: bool,
}
```

## Object<T> Wrapper

```move
struct Object<phantom T> has copy, drop, store {
    inner: address  // Pointer to object
}
```

## Object Creation

### Named Objects (Deterministic)

```move
let constructor_ref = object::create_named_object(creator, b"SEED");
// Address = hash(creator_address, seed)
```

### Generated Objects (Random)

```move
let constructor_ref = object::create_object(creator);
// Non-deterministic address
```

### Sticky Objects (Cannot Delete)

```move
let constructor_ref = object::create_sticky_object(creator);
```

## References (Capabilities)

### ConstructorRef - Master Key (Creation Only)

```move
let constructor_ref = object::create_object(creator);

// Generate all other refs during creation
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 object_signer = object::generate_signer(&constructor_ref);

// Store refs at object address
move_to(&object_signer, Refs { extend_ref, transfer_ref, delete_ref });
```

### ExtendRef - Access Later

```move
// Get signer after creation
let object_signer = object::generate_signer_for_extending(&refs.extend_ref);
```

### TransferRef - Control Transfers

```move
// Disable transfers (soul-bound)
object::disable_ungated_transfer(&refs.transfer_ref);

// Enable transfers
object::enable_ungated_transfer(&refs.transfer_ref);

// Force transfer
object::transfer_with_ref(&refs.transfer_ref, new_owner);
```

### DeleteRef - Destroy Objects

```move
// Remove all resources first, then delete
let MyResource { value: _ } = move_from<MyResource>(object_addr);
object::delete(delete_ref);
```

## Ownership & Transfer

```move
// Get object info
let object_addr = object::object_address(&obj);
let owner = object::owner(obj);
let transferable = object::ungated_transfer_allowed(obj);

// User transfer (if allowed)
object::transfer(owner, obj, new_owner);
```

## Common Patterns

### Soul-Bound Token

```move
public fun create_sbt(creator: &signer, recipient: address) {
    let constructor_ref = token::create_named_token(...);
    
    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    object::disable_ungated_transfer(&transfer_ref);
    
    // One-time transfer to recipient
    let linear_ref = object::generate_linear_transfer_ref(&transfer_ref);
    object::transfer_with_ref(linear_ref, recipient);
    
    // Don't store transfer_ref - permanently non-transferable
}
```

### Composable NFT (Nesting)

```move
// Create parent
let parent_ref = token::create_named_token(...);
let parent_addr = object::address_from_constructor_ref(&parent_ref);

// Create child owned by parent
let child_ref = token::create_named_token(...);
let transfer_ref = object::generate_transfer_ref(&child_ref);
let linear_ref = object::generate_linear_transfer_ref(&transfer_ref);
object::transfer_with_ref(linear_ref, parent_addr);
```

### Singleton/Registry

```move
public fun get_or_create_registry(creator: &signer): address {
    let seed = b"REGISTRY";
    let addr = object::create_object_address(&signer::address_of(creator), seed);
    
    if (!object::object_exists<Registry>(addr)) {
        let ref = object::create_named_object(creator, seed);
        let signer = object::generate_signer(&ref);
        move_to(&signer, Registry { items: simple_map::create() });
    };
    
    addr
}
```

### Upgradeable Module Data

```move
struct DataRefs has key { extend_ref: ExtendRef }

public fun upgrade_config(new_config: Config) acquires DataRefs {
    let refs = borrow_global<DataRefs>(data_addr);
    let signer = object::generate_signer_for_extending(&refs.extend_ref);
    // Modify resources at data_addr
}
```

## Best Practices

- Store refs at object address, not creator address
- Generate all needed refs during creation (ConstructorRef is ephemeral)
- Use named objects for singletons, generated for collections
- Disable ungated transfer for soul-bound tokens
- Clean up all resources before deletion
- Document object ownership hierarchy

## Common Errors

### "Object does not exist"
```move
assert!(object::object_exists<T>(addr), ERROR);
```

### "Ungated transfer not allowed"
```move
// Use TransferRef for forced transfer
object::transfer_with_ref(&refs.transfer_ref, recipient);
```

### "Object already exists"
```move
// Check existence before creating named object
if (!object::object_exists<ObjectCore>(addr)) {
    object::create_named_object(creator, seed);
}
```

Overview

This skill is an expert guide to the Aptos Object Model for building composable, transferable on-chain assets. It explains ObjectCore, the Object<T> wrapper, ConstructorRef/ExtendRef/TransferRef/DeleteRef capabilities, ownership models, named vs generated objects, and composability patterns. Practical examples show common patterns like soul-bound tokens, nesting, registries, and upgradeable data.

How this skill works

The skill inspects how objects are created, addressed, and managed via constructor references and derived capability refs. It explains deterministic named objects versus non-deterministic generated objects, how refs are stored at the object address, and how transfer/delete/extend operations are gated by specific refs. It also covers lifecycle flows: create, extend, transfer, and delete while preserving ownership separation between owner address and object address.

When to use it

  • Designing transferable or composable NFTs and tokens
  • Creating soul-bound tokens or intentionally non-transferable assets
  • Building nested/composable assets where objects own other objects
  • Implementing singletons or registries using deterministic addresses
  • Managing upgradeable on-chain module data with controlled extend capabilities

Best practices

  • Generate all needed refs at creation; treat ConstructorRef as ephemeral
  • Store capability refs at the object address rather than the creator address
  • Use named (seeded) objects for singletons and deterministic resources; use generated objects for collections
  • Disable ungated transfer for soul-bound assets and use linear transfer refs for one-time transfers
  • Always remove or move out resources before calling delete on an object; document ownership hierarchies clearly

Example use cases

  • Soul-bound identity token: create named object, disable ungated transfer, perform a one-time linear transfer to recipient
  • Composable NFT: create parent object, mint child object, transfer child to parent address to nest
  • Registry singleton: use a named object with a registry struct stored at the object address
  • Upgradeable module config: store ExtendRef in a DataRefs resource and generate signer for controlled upgrades
  • Force-recover asset: use TransferRef.transfer_with_ref to move assets in emergency

FAQ

Why store refs at the object address instead of the creator?

Storing refs at the object address ensures capability ownership follows the object lifecycle and prevents creator-only control after transfer.

When should I use named vs generated objects?

Use named objects for deterministic singletons or registries keyed by seed; use generated objects for unique, non-deterministic assets or large collections.

How do I make an object permanently non-transferable?

Disable ungated transfer via the TransferRef and avoid storing any transferable refs; for one-time assignment, use a linear transfer ref and then discard it.