Optimizing Storage Lookup
Ownership type macros allow developers to declare an object’s ownership type—shared, frozen, or owned—at compile time using the #[ext(shared_objects(..))], #[ext(frozen_objects(..))] and #[ext(owned_objects(..))] attributes respectively. This explicit declaration reduces gas costs by optimizing storage retrieval. Without these macros, the runtime must perform a sequential search: first checking the transaction signer’s storage, then the shared namespace, and finally the frozen namespace. By specifying the ownership type beforehand, the system bypasses these redundant lookups and maps directly to the correct storage segment.
Usage Syntax
The three macros function identically: within a function definition, you specify the ownership type of any object by placing the argument name into the corresponding macro category.
In the example below, the macro informs the compiler exactly which storage mapping to use for each object, regardless of the order in which they appear in the function arguments:
#[ext(owned_objects(owned_1, owned_2), shared_objects(shared), frozen_objects(frozen))]
entry fun do_something_fn(
owned_1: &OwnedObject,
shared: &SharedObject,
frozen: &FrozenObject
owned_2:&OwnedObject
) {
}
Note
When multiple objects share the same ownership type, their argument names should be grouped as a comma-separated list within a single macro attribute.
Warning
Assigning an incorrect ownership type will prevent the runtime from locating the object in storage, resulting in a runtime error. Use these macros with caution for objects whose ownership (e.g., transitioning from owned to shared) may change over time.
Example
In the following example, we define three distinct storage objects.
- Two of them have a
NamedIdas first field and reside in the Shared and Frozen namespaces correspondingly. - The third is a standard Owned object transferred to the
sender()upon creation.
By utilizing the #[ext(...)] attribute macro in the do_something_fn function, we know exactly where to find each object, ensuring the most gas-efficient execution path.
module test::ownership_macros;
use stylus::{
tx_context::TxContext,
object::{Self, UID, NamedId},
transfer::{Self}
};
// Marker types for NamedIds
public struct SHARED_OBJECT_ {}
public struct FROZEN_OBJECT_ {}
/// A shared object accessible by any user
public struct SharedObject has key {
id: NamedId<SHARED_OBJECT_>,
a: u8,
}
/// A frozen object (read-only)
public struct FrozenObject has key {
id: NamedId<FROZEN_OBJECT_>,
a: u16,
}
/// An owned object belonging to a specific address
public struct OwnedObject has key {
id: UID,
a: u32,
}
/// Initializes the three objects and assigns their ownership status
entry fun init(ctx: &mut TxContext) {
// Shared: Accessible globally
transfer::share_object(SharedObject {
id: object::new_named_id<SHARED_OBJECT_>(),
a: 2
});
// Frozen: Becomes immutable and read-only
transfer::freeze_object(FrozenObject {
id: object::new_named_id<FROZEN_OBJECT_>(),
a: 3
});
// Owned: Transferred to the transaction sender
transfer::transfer(OwnedObject {
id: object::new(ctx),
a: 5
}, ctx.sender());
}
/// Uses Ownership Macros to optimize gas by specifying object namespaces at compile time
#[ext(owned_objects(owned), shared_objects(shared), frozen_objects(frozen))]
entry fun do_something_fn(
owned: &OwnedObject,
shared: &SharedObject,
frozen: &FrozenObject
) {
// The runtime jumps directly to each object's storage location
// without performing a sequential search.
}