Agent skill
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 this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/aptos-object-model
SKILL.md
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:
- Wraps resources - Any resource can become an "object"
- Enables transfer - Objects can be transferred between accounts
- Provides composability - Objects can own other objects
- Manages lifecycle - Creation, extension, deletion via refs
- 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:
ObjectCoreis 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
Tindicates 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
Didn't find tool you were looking for?