Object Model
The Aptos object model introduces a powerful abstraction for managing complex digital assets with rich composition, flexible ownership, and extensible functionality. Objects provide globally addressable, heterogeneous resources that support ownership hierarchies, reference relationships, and safe composition patterns not easily achievable with traditional Move resources.
Overview#
Objects in Aptos are special on-chain entities identified by unique addresses, combining the benefits of resource safety with flexible ownership and composition capabilities. Unlike traditional resources stored directly under user accounts, objects have their own addresses and can own other objects, creating hierarchical ownership structures ideal for NFTs, composable game items, and complex DeFi positions.
Core Concepts#
Object Creation#
Objects are created with unique addresses and can store multiple heterogeneous resources:
module 0x1::nft_collection {
use std::string::String;
use aptos_framework::object::{Self, Object, ConstructorRef};
use aptos_token_objects::token;
use aptos_token_objects::collection;
struct CollectionMetadata has key {
creator: address,
description: String,
max_supply: u64,
minted: u64
}
struct TokenMetadata has key {
name: String,
description: String,
uri: String,
rarity: u8
}
// Create a collection object
public entry fun create_collection(
creator: &signer,
name: String,
description: String,
max_supply: u64,
uri: String
) {
let constructor_ref = collection::create_unlimited_collection(
creator,
description,
name,
option::none(),
uri
);
let object_signer = object::generate_signer(&constructor_ref);
move_to(&object_signer, CollectionMetadata {
creator: signer::address_of(creator),
description,
max_supply,
minted: 0
});
}
// Create an NFT object within the collection
public entry fun mint_nft(
creator: &signer,
collection: String,
name: String,
description: String,
uri: String,
rarity: u8
) {
let constructor_ref = token::create_named_token(
creator,
collection,
description,
name,
option::none(),
uri
);
let object_signer = object::generate_signer(&constructor_ref);
move_to(&object_signer, TokenMetadata {
name,
description,
uri,
rarity
});
}
}
Key Features#
Object References#
Objects can reference other objects safely without ownership transfer:
struct GameCharacter has key {
name: String,
level: u64,
equipped_weapon: Object<Weapon>, // Reference to weapon object
inventory: vector<Object<Item>> // References to item objects
}
public fun equip_weapon(
character_obj: Object<GameCharacter>,
weapon_obj: Object<Weapon>
) acquires GameCharacter {
let character = borrow_global_mut<GameCharacter>(object::object_address(&character_obj));
character.equipped_weapon = weapon_obj;
}
Ownership Transfer#
Objects support flexible ownership patterns:
use aptos_framework::object;
public entry fun transfer_nft(
owner: &signer,
nft: Object<TokenMetadata>,
recipient: address
) {
// Transfer object ownership
object::transfer(owner, nft, recipient);
}
public entry fun make_soulbound(creator: &signer, nft: Object<TokenMetadata>) {
// Disable transfers permanently
let transfer_ref = object::generate_transfer_ref(creator, nft);
object::disable_ungated_transfer(&transfer_ref);
}
Object Composition#
Objects can own other objects, creating hierarchies:
struct Bundle has key {
items: vector<Object<Item>>,
total_value: u64
}
public fun create_bundle(
creator: &signer,
item_objects: vector<Object<Item>>
) {
let constructor_ref = object::create_object(signer::address_of(creator));
let object_signer = object::generate_signer(&constructor_ref);
// Transfer items to the bundle object
let i = 0;
while (i < vector::length(&item_objects)) {
let item = *vector::borrow(&item_objects, i);
object::transfer(creator, item, signer::address_of(&object_signer));
i = i + 1;
};
move_to(&object_signer, Bundle {
items: item_objects,
total_value: 0 // Calculate from items
});
}
Real-World Use Cases#
-
Composable NFTs: Create NFTs that can own other NFTs, like a character owning equipment, a house containing furniture, or a card deck containing individual cards.
-
DeFi Positions: Represent complex financial positions as objects that aggregate multiple assets, track performance, and enable atomic position transfers.
-
Gaming Assets: Build rich game systems where items, characters, and locations are objects with properties, inventories, and relationships.
-
Fractional Ownership: Create objects representing shared ownership of assets where multiple parties hold stakes in a single valuable item.
-
Licensing and Royalties: Implement objects that track usage rights, royalty obligations, and derivative relationships between creative works.
-
Supply Chain Tracking: Model physical goods as objects that move through a supply chain, accumulating provenance and certification data.
Best Practices#
Use Objects for Complex Assets: Prefer objects over simple resources when assets need ownership transfer, composition, or references to other assets.
Leverage Object Addresses: Objects have stable addresses independent of owner, enabling reliable references and off-chain indexing.
Implement Access Control: Use object extensions and permissions to control who can modify object properties or transfer ownership.
Consider Gas Costs: Object operations involve additional overhead compared to simple resources; balance flexibility with efficiency.
Plan Ownership Hierarchies: Design clear ownership structures to prevent circular references and simplify asset management.
Utilize Events: Emit events for object creation, transfer, and modification to enable off-chain tracking and indexing.
Test Composition Patterns: Thoroughly test complex object hierarchies to ensure proper cleanup and prevent orphaned resources.
Object Capabilities#
Objects support various capabilities through refs:
// Transfer control
let transfer_ref = object::generate_transfer_ref(&constructor_ref);
object::disable_ungated_transfer(&transfer_ref);
// Deletion control
let delete_ref = object::generate_delete_ref(&constructor_ref);
object::delete(delete_ref);
// Extension control
let extend_ref = object::generate_extend_ref(&constructor_ref);
let object_signer = object::generate_signer_for_extending(&extend_ref);
Querying Objects#
#[view]
public fun get_owner(obj: Object<TokenMetadata>): address {
object::owner(obj)
}
#[view]
public fun is_owner(obj: Object<TokenMetadata>, potential_owner: address): bool {
object::is_owner(obj, potential_owner)
}
#[view]
public fun can_transfer(obj: Object<TokenMetadata>): bool {
object::ungated_transfer_allowed(obj)
}
Object vs Traditional Resources#
Use Objects When:
- Assets need to be transferred between users
- Complex composition or hierarchies are required
- Stable addresses independent of owner are beneficial
- References between assets are needed
Use Traditional Resources When:
- Simple account-scoped data storage
- No transfer or ownership changes needed
- Minimizing gas costs is critical
- Simple key-value storage suffices
Related Concepts#
- Resource Management - Traditional resource patterns
- Multi-Agent Transactions - Transfer objects atomically
- View Functions - Query object properties
- Testing - Test object composition