Skip to content

object - [mainnet]

This defines the Move object model with the following properties:

  • Simplified storage interface that supports a heterogeneous collection of resources to be stored together. This enables data types to share a common core data layer (e.g., tokens), while having richer extensions (e.g., concert ticket, sword).
  • Globally accessible data and ownership model that enables creators and developers to dictate the application and lifetime of data.
  • Extensible programming model that supports individualization of user applications that leverage the core framework including tokens.
  • Support emitting events directly, thus improving discoverability of events associated with objects.
  • Considerate of the underlying system by leveraging resource groups for gas efficiency, avoiding costly deserialization and serialization costs, and supporting deletability.

TODO:

  • There is no means to borrow an object or a reference to an object. We are exploring how to make it so that a reference to a global object can be returned from a function.
use 0x1::account;
use 0x1::bcs;
use 0x1::create_signer;
use 0x1::error;
use 0x1::event;
use 0x1::features;
use 0x1::from_bcs;
use 0x1::guid;
use 0x1::hash;
use 0x1::permissioned_signer;
use 0x1::signer;
use 0x1::transaction_context;
use 0x1::vector;

Constants

Address where unwanted objects can be forcefully transferred to.

const BURN_ADDRESS: address = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

generate_unique_address uses this for domain separation within its native implementation

const DERIVE_AUID_ADDRESS_SCHEME: u8 = 251;

The object does not allow for deletion

const ECANNOT_DELETE: u64 = 5;

Exceeds maximum nesting for an object transfer.

const EMAXIMUM_NESTING: u64 = 6;

The caller does not have ownership permissions

const ENOT_OBJECT_OWNER: u64 = 4;

The object does not have ungated transfers enabled

const ENO_UNGATED_TRANSFERS: u64 = 3;

Cannot burn an object that is already burnt.

const EOBJECT_ALREADY_BURNT: u64 = 10;

An object does not exist at this address

const EOBJECT_DOES_NOT_EXIST: u64 = 2;

An object already exists at this address

const EOBJECT_EXISTS: u64 = 1;

Cannot reclaim objects that weren’t burnt.

const EOBJECT_NOT_BURNT: u64 = 8;

Object is untransferable any operations that might result in a transfer are disallowed.

const EOBJECT_NOT_TRANSFERRABLE: u64 = 9;

The resource is not stored at the specified address.

const ERESOURCE_DOES_NOT_EXIST: u64 = 7;

Explicitly separate the GUID space between Object and Account to prevent accidental overlap.

const INIT_GUID_CREATION_NUM: u64 = 1125899906842624;

Maximum nesting from one object to another. That is objects can technically have infinte nesting, but any checks such as transfer will only be evaluated this deep.

const MAXIMUM_OBJECT_NESTING: u8 = 8;

Scheme identifier used to generate an object’s address obj_addr as derived from another object. The object’s address is generated as:

obj_addr = sha3_256(account addr | derived from object's address | 0xFC)

This 0xFC constant serves as a domain separation tag to prevent existing authentication key and resource account derivation to produce an object address.

const OBJECT_DERIVED_SCHEME: u8 = 252;

Scheme identifier used to generate an object’s address obj_addr via a fresh GUID generated by the creator at source_addr. The object’s address is generated as:

obj_addr = sha3_256(guid | 0xFD)

where guid = account::create_guid(create_signer(source_addr))

This 0xFD constant serves as a domain separation tag to prevent existing authentication key and resource account derivation to produce an object address.

const OBJECT_FROM_GUID_ADDRESS_SCHEME: u8 = 253;

Scheme identifier used to generate an object’s address obj_addr from the creator’s source_addr and a seed as: obj_addr = sha3_256(source_addr | seed | 0xFE).

This 0xFE constant serves as a domain separation tag to prevent existing authentication key and resource account derivation to produce an object address.

const OBJECT_FROM_SEED_ADDRESS_SCHEME: u8 = 254;

Structs

ObjectGroup

A shared resource group for storing object resources together in storage.

#[resource_group(#[scope = global])]
struct ObjectGroup
Fields
dummy_field: bool

Object

A pointer to an object — these can only provide guarantees based upon the underlying data type, that is the validity of T existing at an address is something that cannot be verified by any other module than the module that defined T. Similarly, the module that defines T can remove it from storage at any point in time.

struct Object<T> has copy, drop, store
Fields
inner: address

ConstructorRef

This is a one time ability given to the creator to configure the object as necessary

struct ConstructorRef has drop
Fields
self: address
can_delete: bool
True if the object can be deleted. Named objects are not deletable.

DeleteRef

Used to remove an object from storage.

struct DeleteRef has drop, store
Fields
self: address

ExtendRef

Used to create events or move additional resources into object storage.

struct ExtendRef has drop, store
Fields
self: address

TransferRef

Used to create LinearTransferRef, hence ownership transfer.

struct TransferRef has drop, store
Fields
self: address

LinearTransferRef

Used to perform transfers. This locks transferring ability to a single time use bound to the current owner.

struct LinearTransferRef has drop
Fields
self: address
owner: address

DeriveRef

Used to create derived objects from a given objects.

struct DeriveRef has drop, store
Fields
self: address

TransferPermission

Permission to transfer object with permissioned signer.

struct TransferPermission has copy, drop, store
Fields
object: address

TransferEvent

Emitted whenever the object’s owner field is changed.

struct TransferEvent has drop, store
Fields
object: address
from: address
to: address

Transfer

Emitted whenever the object’s owner field is changed.

#[event]
struct Transfer has drop, store
Fields
object: address
from: address
to: address

Resources

ObjectCore

The core of the object model that defines ownership, transferability, and events.

#[resource_group_member(#[group = 0x1::object::ObjectGroup])]
struct ObjectCore has key
Fields
guid_creation_num: u64
Used by guid to guarantee globally unique objects and create event streams
owner: address
The address (object or account) that owns this object
allow_ungated_transfer: bool
Object transferring is a common operation, this allows for disabling and enabling transfers bypassing the use of a TransferRef.
transfer_events: event::EventHandle<object::TransferEvent>
Emitted events upon transferring of ownership.

TombStone

This is added to objects that are burnt (ownership transferred to BURN_ADDRESS).

#[resource_group_member(#[group = 0x1::object::ObjectGroup])]
struct TombStone has key
Fields
original_owner: address
Track the previous owner before the object is burnt so they can reclaim later if so desired.

Untransferable

The existence of this renders all TransferRefs irrelevant. The object cannot be moved.

#[resource_group_member(#[group = 0x1::object::ObjectGroup])]
struct Untransferable has key
Fields
dummy_field: bool

Functions

is_untransferable

#[view]
public fun is_untransferable<T: key>(object: object::Object<T>): bool
Implementation
public fun is_untransferable<T: key>(object: Object<T>): bool {
exists<Untransferable>(object.inner)
}

is_burnt

#[view]
public fun is_burnt<T: key>(object: object::Object<T>): bool
Implementation
public fun is_burnt<T: key>(object: Object<T>): bool {
exists<TombStone>(object.inner)
}

address_to_object

Produces an ObjectId from the given address. This is not verified.

public fun address_to_object<T: key>(object: address): object::Object<T>
Implementation
public fun address_to_object<T: key>(object: address): Object<T> {
assert!(exists<ObjectCore>(object), error::not_found(EOBJECT_DOES_NOT_EXIST));
assert!(exists_at<T>(object), error::not_found(ERESOURCE_DOES_NOT_EXIST));
Object<T> { inner: object }
}

is_object

Returns true if there exists an object or the remnants of an object.

public fun is_object(object: address): bool
Implementation
public fun is_object(object: address): bool {
exists<ObjectCore>(object)
}

object_exists

Returns true if there exists an object with resource T.

public fun object_exists<T: key>(object: address): bool
Implementation
public fun object_exists<T: key>(object: address): bool {
exists<ObjectCore>(object) && exists_at<T>(object)
}

create_object_address

Derives an object address from source material: sha3_256([creator address | seed | 0xFE]).

public fun create_object_address(source: &address, seed: vector<u8>): address
Implementation
public fun create_object_address(source: &address, seed: vector<u8>): address {
let bytes = bcs::to_bytes(source);
vector::append(&mut bytes, seed);
vector::push_back(&mut bytes, OBJECT_FROM_SEED_ADDRESS_SCHEME);
from_bcs::to_address(hash::sha3_256(bytes))
}

create_user_derived_object_address_impl

fun create_user_derived_object_address_impl(source: address, derive_from: address): address
Implementation
native fun create_user_derived_object_address_impl(source: address, derive_from: address): address;

create_user_derived_object_address

Derives an object address from the source address and an object: sha3_256([source | object addr | 0xFC]).

public fun create_user_derived_object_address(source: address, derive_from: address): address
Implementation
public fun create_user_derived_object_address(source: address, derive_from: address): address {
if (std::features::object_native_derived_address_enabled()) {
create_user_derived_object_address_impl(source, derive_from)
} else {
let bytes = bcs::to_bytes(&source);
vector::append(&mut bytes, bcs::to_bytes(&derive_from));
vector::push_back(&mut bytes, OBJECT_DERIVED_SCHEME);
from_bcs::to_address(hash::sha3_256(bytes))
}
}

create_guid_object_address

Derives an object from an Account GUID.

public fun create_guid_object_address(source: address, creation_num: u64): address
Implementation
public fun create_guid_object_address(source: address, creation_num: u64): address {
let id = guid::create_id(source, creation_num);
let bytes = bcs::to_bytes(&id);
vector::push_back(&mut bytes, OBJECT_FROM_GUID_ADDRESS_SCHEME);
from_bcs::to_address(hash::sha3_256(bytes))
}

exists_at

fun exists_at<T: key>(object: address): bool
Implementation
native fun exists_at<T: key>(object: address): bool;

object_address

Returns the address of within an ObjectId.

public fun object_address<T: key>(object: &object::Object<T>): address
Implementation
public fun object_address<T: key>(object: &Object<T>): address {
object.inner
}

convert

Convert Object to Object.

public fun convert<X: key, Y: key>(object: object::Object<X>): object::Object<Y>
Implementation
public fun convert<X: key, Y: key>(object: Object<X>): Object<Y> {
address_to_object<Y>(object.inner)
}

create_named_object

Create a new named object and return the ConstructorRef. Named objects can be queried globally by knowing the user generated seed used to create them. Named objects cannot be deleted.

public fun create_named_object(creator: &signer, seed: vector<u8>): object::ConstructorRef
Implementation
public fun create_named_object(creator: &signer, seed: vector<u8>): ConstructorRef {
let creator_address = signer::address_of(creator);
let obj_addr = create_object_address(&creator_address, seed);
create_object_internal(creator_address, obj_addr, false)
}

create_user_derived_object

Create a new object whose address is derived based on the creator account address and another object. Derivde objects, similar to named objects, cannot be deleted.

public(friend) fun create_user_derived_object(creator_address: address, derive_ref: &object::DeriveRef): object::ConstructorRef
Implementation
public(friend) fun create_user_derived_object(creator_address: address, derive_ref: &DeriveRef): ConstructorRef {
let obj_addr = create_user_derived_object_address(creator_address, derive_ref.self);
create_object_internal(creator_address, obj_addr, false)
}

create_object

Create a new object by generating a random unique address based on transaction hash. The unique address is computed sha3_256([transaction hash | auid counter | 0xFB]). The created object is deletable as we can guarantee the same unique address can never be regenerated with future txs.

public fun create_object(owner_address: address): object::ConstructorRef
Implementation
public fun create_object(owner_address: address): ConstructorRef {
let unique_address = transaction_context::generate_auid_address();
create_object_internal(owner_address, unique_address, true)
}

create_sticky_object

Same as create_object except the object to be created will be undeletable.

public fun create_sticky_object(owner_address: address): object::ConstructorRef
Implementation
public fun create_sticky_object(owner_address: address): ConstructorRef {
let unique_address = transaction_context::generate_auid_address();
create_object_internal(owner_address, unique_address, false)
}

create_sticky_object_at_address

Create a sticky object at a specific address. Only used by aptos_framework::coin.

public(friend) fun create_sticky_object_at_address(owner_address: address, object_address: address): object::ConstructorRef
Implementation
public(friend) fun create_sticky_object_at_address(
owner_address: address,
object_address: address,
): ConstructorRef {
create_object_internal(owner_address, object_address, false)
}

create_object_from_account

Use create_object instead. Create a new object from a GUID generated by an account. As the GUID creation internally increments a counter, two transactions that executes create_object_from_account function for the same creator run sequentially. Therefore, using create_object method for creating objects is preferrable as it doesn’t have the same bottlenecks.

#[deprecated]
public fun create_object_from_account(creator: &signer): object::ConstructorRef
Implementation
public fun create_object_from_account(creator: &signer): ConstructorRef {
let guid = account::create_guid(creator);
create_object_from_guid(signer::address_of(creator), guid)
}

create_object_from_object

Use create_object instead. Create a new object from a GUID generated by an object. As the GUID creation internally increments a counter, two transactions that executes create_object_from_object function for the same creator run sequentially. Therefore, using create_object method for creating objects is preferrable as it doesn’t have the same bottlenecks.

#[deprecated]
public fun create_object_from_object(creator: &signer): object::ConstructorRef
Implementation
public fun create_object_from_object(creator: &signer): ConstructorRef acquires ObjectCore {
let guid = create_guid(creator);
create_object_from_guid(signer::address_of(creator), guid)
}

create_object_from_guid

fun create_object_from_guid(creator_address: address, guid: guid::GUID): object::ConstructorRef
Implementation
fun create_object_from_guid(creator_address: address, guid: guid::GUID): ConstructorRef {
let bytes = bcs::to_bytes(&guid);
vector::push_back(&mut bytes, OBJECT_FROM_GUID_ADDRESS_SCHEME);
let obj_addr = from_bcs::to_address(hash::sha3_256(bytes));
create_object_internal(creator_address, obj_addr, true)
}

create_object_internal

fun create_object_internal(creator_address: address, object: address, can_delete: bool): object::ConstructorRef
Implementation
fun create_object_internal(
creator_address: address,
object: address,
can_delete: bool,
): ConstructorRef {
assert!(!exists<ObjectCore>(object), error::already_exists(EOBJECT_EXISTS));
let object_signer = create_signer(object);
let guid_creation_num = INIT_GUID_CREATION_NUM;
let transfer_events_guid = guid::create(object, &mut guid_creation_num);
move_to(
&object_signer,
ObjectCore {
guid_creation_num,
owner: creator_address,
allow_ungated_transfer: true,
transfer_events: event::new_event_handle(transfer_events_guid),
},
);
ConstructorRef { self: object, can_delete }
}

generate_delete_ref

Generates the DeleteRef, which can be used to remove ObjectCore from global storage.

public fun generate_delete_ref(ref: &object::ConstructorRef): object::DeleteRef
Implementation
public fun generate_delete_ref(ref: &ConstructorRef): DeleteRef {
assert!(ref.can_delete, error::permission_denied(ECANNOT_DELETE));
DeleteRef { self: ref.self }
}

generate_extend_ref

Generates the ExtendRef, which can be used to add new events and resources to the object.

public fun generate_extend_ref(ref: &object::ConstructorRef): object::ExtendRef
Implementation
public fun generate_extend_ref(ref: &ConstructorRef): ExtendRef {
ExtendRef { self: ref.self }
}

generate_transfer_ref

Generates the TransferRef, which can be used to manage object transfers.

public fun generate_transfer_ref(ref: &object::ConstructorRef): object::TransferRef
Implementation
public fun generate_transfer_ref(ref: &ConstructorRef): TransferRef {
assert!(!exists<Untransferable>(ref.self), error::permission_denied(EOBJECT_NOT_TRANSFERRABLE));
TransferRef { self: ref.self }
}

generate_derive_ref

Generates the DeriveRef, which can be used to create determnistic derived objects from the current object.

public fun generate_derive_ref(ref: &object::ConstructorRef): object::DeriveRef
Implementation
public fun generate_derive_ref(ref: &ConstructorRef): DeriveRef {
DeriveRef { self: ref.self }
}

generate_signer

Create a signer for the ConstructorRef

public fun generate_signer(ref: &object::ConstructorRef): signer
Implementation
public fun generate_signer(ref: &ConstructorRef): signer {
create_signer(ref.self)
}

address_from_constructor_ref

Returns the address associated with the constructor

public fun address_from_constructor_ref(ref: &object::ConstructorRef): address
Implementation
public fun address_from_constructor_ref(ref: &ConstructorRef): address {
ref.self
}

object_from_constructor_ref

Returns an Object from within a ConstructorRef

public fun object_from_constructor_ref<T: key>(ref: &object::ConstructorRef): object::Object<T>
Implementation
public fun object_from_constructor_ref<T: key>(ref: &ConstructorRef): Object<T> {
address_to_object<T>(ref.self)
}

can_generate_delete_ref

Returns whether or not the ConstructorRef can be used to create DeleteRef

public fun can_generate_delete_ref(ref: &object::ConstructorRef): bool
Implementation
public fun can_generate_delete_ref(ref: &ConstructorRef): bool {
ref.can_delete
}

create_guid

Create a guid for the object, typically used for events

public fun create_guid(object: &signer): guid::GUID
Implementation
public fun create_guid(object: &signer): guid::GUID acquires ObjectCore {
let addr = signer::address_of(object);
let object_data = borrow_global_mut<ObjectCore>(addr);
guid::create(addr, &mut object_data.guid_creation_num)
}

new_event_handle

Generate a new event handle.

public fun new_event_handle<T: drop, store>(object: &signer): event::EventHandle<T>
Implementation
public fun new_event_handle<T: drop + store>(
object: &signer,
): event::EventHandle<T> acquires ObjectCore {
event::new_event_handle(create_guid(object))
}

address_from_delete_ref

Returns the address associated with the constructor

public fun address_from_delete_ref(ref: &object::DeleteRef): address
Implementation
public fun address_from_delete_ref(ref: &DeleteRef): address {
ref.self
}

object_from_delete_ref

Returns an Object from within a DeleteRef.

public fun object_from_delete_ref<T: key>(ref: &object::DeleteRef): object::Object<T>
Implementation
public fun object_from_delete_ref<T: key>(ref: &DeleteRef): Object<T> {
address_to_object<T>(ref.self)
}

delete

Removes from the specified Object from global storage.

public fun delete(ref: object::DeleteRef)
Implementation
public fun delete(ref: DeleteRef) acquires Untransferable, ObjectCore {
let object_core = move_from<ObjectCore>(ref.self);
let ObjectCore {
guid_creation_num: _,
owner: _,
allow_ungated_transfer: _,
transfer_events,
} = object_core;
if (exists<Untransferable>(ref.self)) {
let Untransferable {} = move_from<Untransferable>(ref.self);
};
event::destroy_handle(transfer_events);
}

generate_signer_for_extending

Create a signer for the ExtendRef

public fun generate_signer_for_extending(ref: &object::ExtendRef): signer
Implementation
public fun generate_signer_for_extending(ref: &ExtendRef): signer {
create_signer(ref.self)
}

address_from_extend_ref

Returns an address from within a ExtendRef.

public fun address_from_extend_ref(ref: &object::ExtendRef): address
Implementation
public fun address_from_extend_ref(ref: &ExtendRef): address {
ref.self
}

disable_ungated_transfer

Disable direct transfer, transfers can only be triggered via a TransferRef

public fun disable_ungated_transfer(ref: &object::TransferRef)
Implementation
public fun disable_ungated_transfer(ref: &TransferRef) acquires ObjectCore {
let object = borrow_global_mut<ObjectCore>(ref.self);
object.allow_ungated_transfer = false;
}

set_untransferable

Prevent moving of the object

public fun set_untransferable(ref: &object::ConstructorRef)
Implementation
public fun set_untransferable(ref: &ConstructorRef) acquires ObjectCore {
let object = borrow_global_mut<ObjectCore>(ref.self);
object.allow_ungated_transfer = false;
let object_signer = generate_signer(ref);
move_to(&object_signer, Untransferable {});
}

enable_ungated_transfer

Enable direct transfer.

public fun enable_ungated_transfer(ref: &object::TransferRef)
Implementation
public fun enable_ungated_transfer(ref: &TransferRef) acquires ObjectCore {
assert!(!exists<Untransferable>(ref.self), error::permission_denied(EOBJECT_NOT_TRANSFERRABLE));
let object = borrow_global_mut<ObjectCore>(ref.self);
object.allow_ungated_transfer = true;
}

generate_linear_transfer_ref

Create a LinearTransferRef for a one-time transfer. This requires that the owner at the time of generation is the owner at the time of transferring.

public fun generate_linear_transfer_ref(ref: &object::TransferRef): object::LinearTransferRef
Implementation
public fun generate_linear_transfer_ref(ref: &TransferRef): LinearTransferRef acquires ObjectCore {
assert!(!exists<Untransferable>(ref.self), error::permission_denied(EOBJECT_NOT_TRANSFERRABLE));
let owner = owner(Object<ObjectCore> { inner: ref.self });
LinearTransferRef {
self: ref.self,
owner,
}
}

transfer_with_ref

Transfer to the destination address using a LinearTransferRef.

public fun transfer_with_ref(ref: object::LinearTransferRef, to: address)
Implementation
public fun transfer_with_ref(ref: LinearTransferRef, to: address) acquires ObjectCore, TombStone {
assert!(!exists<Untransferable>(ref.self), error::permission_denied(EOBJECT_NOT_TRANSFERRABLE));
// Undo soft burn if present as we don't want the original owner to be able to reclaim by calling unburn later.
if (exists<TombStone>(ref.self)) {
let TombStone { original_owner: _ } = move_from<TombStone>(ref.self);
};
let object = borrow_global_mut<ObjectCore>(ref.self);
assert!(
object.owner == ref.owner,
error::permission_denied(ENOT_OBJECT_OWNER),
);
if (std::features::module_event_migration_enabled()) {
event::emit(
Transfer {
object: ref.self,
from: object.owner,
to,
},
);
} else {
event::emit_event(
&mut object.transfer_events,
TransferEvent {
object: ref.self,
from: object.owner,
to,
},
);
};
object.owner = to;
}

transfer_call

Entry function that can be used to transfer, if allow_ungated_transfer is set true.

public entry fun transfer_call(owner: &signer, object: address, to: address)
Implementation
public entry fun transfer_call(
owner: &signer,
object: address,
to: address,
) acquires ObjectCore {
transfer_raw(owner, object, to)
}

transfer

Transfers ownership of the object (and all associated resources) at the specified address for Object to the “to” address.

public entry fun transfer<T: key>(owner: &signer, object: object::Object<T>, to: address)
Implementation
public entry fun transfer<T: key>(
owner: &signer,
object: Object<T>,
to: address,
) acquires ObjectCore {
transfer_raw(owner, object.inner, to)
}

transfer_raw

Attempts to transfer using addresses only. Transfers the given object if allow_ungated_transfer is set true. Note, that this allows the owner of a nested object to transfer that object, so long as allow_ungated_transfer is enabled at each stage in the hierarchy.

public fun transfer_raw(owner: &signer, object: address, to: address)
Implementation
public fun transfer_raw(
owner: &signer,
object: address,
to: address,
) acquires ObjectCore {
let owner_address = signer::address_of(owner);
assert!(
permissioned_signer::check_permission_exists(owner, TransferPermission { object }),
error::permission_denied(EOBJECT_NOT_TRANSFERRABLE)
);
verify_ungated_and_descendant(owner_address, object);
transfer_raw_inner(object, to);
}

transfer_raw_inner

fun transfer_raw_inner(object: address, to: address)
Implementation
inline fun transfer_raw_inner(object: address, to: address) acquires ObjectCore {
let object_core = borrow_global_mut<ObjectCore>(object);
if (object_core.owner != to) {
if (std::features::module_event_migration_enabled()) {
event::emit(
Transfer {
object,
from: object_core.owner,
to,
},
);
} else {
event::emit_event(
&mut object_core.transfer_events,
TransferEvent {
object,
from: object_core.owner,
to,
},
);
};
object_core.owner = to;
};
}

transfer_to_object

Transfer the given object to another object. See transfer for more information.

public entry fun transfer_to_object<O: key, T: key>(owner: &signer, object: object::Object<O>, to: object::Object<T>)
Implementation
public entry fun transfer_to_object<O: key, T: key>(
owner: &signer,
object: Object<O>,
to: Object<T>,
) acquires ObjectCore {
transfer(owner, object, to.inner)
}

verify_ungated_and_descendant

This checks that the destination address is eventually owned by the owner and that each object between the two allows for ungated transfers. Note, this is limited to a depth of 8 objects may have cyclic dependencies.

fun verify_ungated_and_descendant(owner: address, destination: address)
Implementation
fun verify_ungated_and_descendant(owner: address, destination: address) acquires ObjectCore {
let current_address = destination;
assert!(
exists<ObjectCore>(current_address),
error::not_found(EOBJECT_DOES_NOT_EXIST),
);
let object = borrow_global<ObjectCore>(current_address);
assert!(
object.allow_ungated_transfer,
error::permission_denied(ENO_UNGATED_TRANSFERS),
);
let current_address = object.owner;
let count = 0;
while (owner != current_address) {
count = count + 1;
assert!(count < MAXIMUM_OBJECT_NESTING, error::out_of_range(EMAXIMUM_NESTING));
// At this point, the first object exists and so the more likely case is that the
// object's owner is not an object. So we return a more sensible error.
assert!(
exists<ObjectCore>(current_address),
error::permission_denied(ENOT_OBJECT_OWNER),
);
let object = borrow_global<ObjectCore>(current_address);
assert!(
object.allow_ungated_transfer,
error::permission_denied(ENO_UNGATED_TRANSFERS),
);
current_address = object.owner;
};
}

burn

Add a TombStone to the object. The object will then be interpreted as hidden via indexers. This only works for objects directly owned and for simplicity does not apply to indirectly owned objects. Original owners can reclaim burnt objects any time in the future by calling unburn. Please use the test only [object::burn_object_with_transfer] for testing with previously burned objects.

public entry fun burn<T: key>(owner: &signer, object: object::Object<T>)
Implementation
public entry fun burn<T: key>(owner: &signer, object: Object<T>) acquires ObjectCore {
let original_owner = signer::address_of(owner);
assert!(is_owner(object, original_owner), error::permission_denied(ENOT_OBJECT_OWNER));
let object_addr = object.inner;
assert!(!exists<TombStone>(object_addr), EOBJECT_ALREADY_BURNT);
move_to(&create_signer(object_addr), TombStone { original_owner });
}

unburn

Allow origin owners to reclaim any objects they previous burnt.

public entry fun unburn<T: key>(original_owner: &signer, object: object::Object<T>)
Implementation
public entry fun unburn<T: key>(
original_owner: &signer,
object: Object<T>,
) acquires TombStone, ObjectCore {
let object_addr = object.inner;
assert!(exists<TombStone>(object_addr), error::invalid_argument(EOBJECT_NOT_BURNT));
assert!(
permissioned_signer::check_permission_exists(original_owner, TransferPermission { object: object_addr }),
error::permission_denied(EOBJECT_NOT_TRANSFERRABLE)
);
// The new owner of the object can always unburn it, but if it's the burn address, we go to the old functionality
let object_core = borrow_global<ObjectCore>(object_addr);
if (object_core.owner == signer::address_of(original_owner)) {
let TombStone { original_owner: _ } = move_from<TombStone>(object_addr);
} else if (object_core.owner == BURN_ADDRESS) {
// The old functionality
let TombStone { original_owner: original_owner_addr } = move_from<TombStone>(object_addr);
assert!(
original_owner_addr == signer::address_of(original_owner),
error::permission_denied(ENOT_OBJECT_OWNER)
);
transfer_raw_inner(object_addr, original_owner_addr);
} else {
abort error::permission_denied(ENOT_OBJECT_OWNER);
};
}

ungated_transfer_allowed

Accessors Return true if ungated transfer is allowed.

public fun ungated_transfer_allowed<T: key>(object: object::Object<T>): bool
Implementation
public fun ungated_transfer_allowed<T: key>(object: Object<T>): bool acquires ObjectCore {
assert!(
exists<ObjectCore>(object.inner),
error::not_found(EOBJECT_DOES_NOT_EXIST),
);
borrow_global<ObjectCore>(object.inner).allow_ungated_transfer
}

owner

Return the current owner.

#[view]
public fun owner<T: key>(object: object::Object<T>): address
Implementation
public fun owner<T: key>(object: Object<T>): address acquires ObjectCore {
assert!(
exists<ObjectCore>(object.inner),
error::not_found(EOBJECT_DOES_NOT_EXIST),
);
borrow_global<ObjectCore>(object.inner).owner
}

is_owner

Return true if the provided address is the current owner.

#[view]
public fun is_owner<T: key>(object: object::Object<T>, owner: address): bool
Implementation
public fun is_owner<T: key>(object: Object<T>, owner: address): bool acquires ObjectCore {
owner(object) == owner
}

owns

Return true if the provided address has indirect or direct ownership of the provided object.

#[view]
public fun owns<T: key>(object: object::Object<T>, owner: address): bool
Implementation
public fun owns<T: key>(object: Object<T>, owner: address): bool acquires ObjectCore {
let current_address = object_address(&object);
assert!(
exists<ObjectCore>(current_address),
error::not_found(EOBJECT_DOES_NOT_EXIST),
);
if (current_address == owner) {
return true
};
let object = borrow_global<ObjectCore>(current_address);
let current_address = object.owner;
let count = 0;
while (owner != current_address) {
count = count + 1;
assert!(count < MAXIMUM_OBJECT_NESTING, error::out_of_range(EMAXIMUM_NESTING));
if (!exists<ObjectCore>(current_address)) {
return false
};
let object = borrow_global<ObjectCore>(current_address);
current_address = object.owner;
};
true
}

root_owner

Returns the root owner of an object. As objects support nested ownership, it can be useful to determine the identity of the starting point of ownership.

#[view]
public fun root_owner<T: key>(object: object::Object<T>): address
Implementation
public fun root_owner<T: key>(object: Object<T>): address acquires ObjectCore {
let obj_owner = owner(object);
while (is_object(obj_owner)) {
obj_owner = owner(address_to_object<ObjectCore>(obj_owner));
};
obj_owner
}

grant_permission

Master signer offers a transfer permission of an object to a permissioned signer.

public fun grant_permission<T>(master: &signer, permissioned_signer: &signer, object: object::Object<T>)
Implementation
public fun grant_permission<T>(
master: &signer,
permissioned_signer: &signer,
object: Object<T>,
) {
permissioned_signer::authorize_unlimited(
master,
permissioned_signer,
TransferPermission { object: object.inner }
)
}

grant_permission_with_transfer_ref

Grant a transfer permission to the permissioned signer using TransferRef.

public fun grant_permission_with_transfer_ref(permissioned_signer: &signer, ref: &object::TransferRef)
Implementation
public fun grant_permission_with_transfer_ref(
permissioned_signer: &signer,
ref: &TransferRef,
) {
permissioned_signer::grant_unlimited_with_permissioned_signer(
permissioned_signer,
TransferPermission { object: ref.self }
)
}

Specification

High-level Requirements

No.RequirementCriticalityImplementationEnforcement
1 It's not possible to create an object twice on the same address. Critical The create_object_internal function includes an assertion to ensure that the object being created does not already exist at the specified address. Formally verified via create_object_internal.
2 Only its owner may transfer an object. Critical The transfer function mandates that the transaction be signed by the owner's address, ensuring that only the rightful owner may initiate the object transfer. Audited that it aborts if anyone other than the owner attempts to transfer.
3 The indirect owner of an object may transfer the object. Medium The owns function evaluates to true when the given address possesses either direct or indirect ownership of the specified object. Audited that it aborts if address transferring is not indirect owner.
4 Objects may never change the address which houses them. Low After creating an object, transfers to another owner may occur. However, the address which stores the object may not be changed. This is implied by high-level requirement 1.
5 If an ungated transfer is disabled on an object in an indirect ownership chain, a transfer should not occur. Medium Calling disable_ungated_transfer disables direct transfer, and only TransferRef may trigger transfers. The transfer_with_ref function is called. Formally verified via transfer_with_ref.
6 Object addresses must not overlap with other addresses in different domains. Critical The current addressing scheme with suffixes does not conflict with any existing addresses, such as resource accounts. The GUID space is explicitly separated to ensure this doesn't happen. This is true by construction if one correctly ensures the usage of INIT_GUID_CREATION_NUM during the creation of GUID.

Module-level Specification

pragma verify = false;
pragma aborts_if_is_partial;

address_to_object

public fun address_to_object<T: key>(object: address): object::Object<T>
aborts_if !exists<ObjectCore>(object);
aborts_if !spec_exists_at<T>(object);
ensures result == Object<T> { inner: object };

create_object_address

public fun create_object_address(source: &address, seed: vector<u8>): address
pragma opaque;
pragma aborts_if_is_strict = false;
aborts_if [abstract] false;
ensures [abstract] result == spec_create_object_address(source, seed);
fun spec_create_user_derived_object_address_impl(source: address, derive_from: address): address;

create_user_derived_object_address_impl

fun create_user_derived_object_address_impl(source: address, derive_from: address): address
pragma opaque;
ensures [abstract] result == spec_create_user_derived_object_address_impl(source, derive_from);

create_user_derived_object_address

public fun create_user_derived_object_address(source: address, derive_from: address): address
pragma opaque;
pragma aborts_if_is_strict = false;
aborts_if [abstract] false;
ensures [abstract] result == spec_create_user_derived_object_address(source, derive_from);

create_guid_object_address

public fun create_guid_object_address(source: address, creation_num: u64): address
pragma opaque;
pragma aborts_if_is_strict = false;
aborts_if [abstract] false;
ensures [abstract] result == spec_create_guid_object_address(source, creation_num);

exists_at

fun exists_at<T: key>(object: address): bool
pragma opaque;
ensures [abstract] result == spec_exists_at<T>(object);

object_address

public fun object_address<T: key>(object: &object::Object<T>): address
aborts_if false;
ensures result == object.inner;

convert

public fun convert<X: key, Y: key>(object: object::Object<X>): object::Object<Y>
aborts_if !exists<ObjectCore>(object.inner);
aborts_if !spec_exists_at<Y>(object.inner);
ensures result == Object<Y> { inner: object.inner };

create_named_object

public fun create_named_object(creator: &signer, seed: vector<u8>): object::ConstructorRef
let creator_address = signer::address_of(creator);
let obj_addr = spec_create_object_address(creator_address, seed);
aborts_if exists<ObjectCore>(obj_addr);
ensures exists<ObjectCore>(obj_addr);
ensures global<ObjectCore>(obj_addr) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: creator_address,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: obj_addr,
}
}
}
};
ensures result == ConstructorRef { self: obj_addr, can_delete: false };

create_user_derived_object

public(friend) fun create_user_derived_object(creator_address: address, derive_ref: &object::DeriveRef): object::ConstructorRef
let obj_addr = spec_create_user_derived_object_address(creator_address, derive_ref.self);
aborts_if exists<ObjectCore>(obj_addr);
ensures exists<ObjectCore>(obj_addr);
ensures global<ObjectCore>(obj_addr) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: creator_address,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: obj_addr,
}
}
}
};
ensures result == ConstructorRef { self: obj_addr, can_delete: false };

create_object

public fun create_object(owner_address: address): object::ConstructorRef
pragma aborts_if_is_partial;
let unique_address = transaction_context::spec_generate_unique_address();
aborts_if exists<ObjectCore>(unique_address);
ensures exists<ObjectCore>(unique_address);
ensures global<ObjectCore>(unique_address) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: owner_address,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: unique_address,
}
}
}
};
ensures result == ConstructorRef { self: unique_address, can_delete: true };

create_sticky_object

public fun create_sticky_object(owner_address: address): object::ConstructorRef
pragma aborts_if_is_partial;
let unique_address = transaction_context::spec_generate_unique_address();
aborts_if exists<ObjectCore>(unique_address);
ensures exists<ObjectCore>(unique_address);
ensures global<ObjectCore>(unique_address) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: owner_address,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: unique_address,
}
}
}
};
ensures result == ConstructorRef { self: unique_address, can_delete: false };

create_sticky_object_at_address

public(friend) fun create_sticky_object_at_address(owner_address: address, object_address: address): object::ConstructorRef
pragma verify = false;

create_object_from_account

#[deprecated]
public fun create_object_from_account(creator: &signer): object::ConstructorRef
aborts_if !exists<account::Account>(signer::address_of(creator));
let object_data = global<account::Account>(signer::address_of(creator));
aborts_if object_data.guid_creation_num + 1 > MAX_U64;
aborts_if object_data.guid_creation_num + 1 >= account::MAX_GUID_CREATION_NUM;
let creation_num = object_data.guid_creation_num;
let addr = signer::address_of(creator);
let guid = guid::GUID {
id: guid::ID {
creation_num,
addr,
}
};
let bytes_spec = bcs::to_bytes(guid);
let bytes = concat(bytes_spec, vec<u8>(OBJECT_FROM_GUID_ADDRESS_SCHEME));
let hash_bytes = hash::sha3_256(bytes);
let obj_addr = from_bcs::deserialize<address>(hash_bytes);
aborts_if exists<ObjectCore>(obj_addr);
aborts_if !from_bcs::deserializable<address>(hash_bytes);
ensures global<account::Account>(addr).guid_creation_num == old(
global<account::Account>(addr)
).guid_creation_num + 1;
ensures exists<ObjectCore>(obj_addr);
ensures global<ObjectCore>(obj_addr) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: addr,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: obj_addr,
}
}
}
};
ensures result == ConstructorRef { self: obj_addr, can_delete: true };

create_object_from_object

#[deprecated]
public fun create_object_from_object(creator: &signer): object::ConstructorRef
aborts_if !exists<ObjectCore>(signer::address_of(creator));
let object_data = global<ObjectCore>(signer::address_of(creator));
aborts_if object_data.guid_creation_num + 1 > MAX_U64;
let creation_num = object_data.guid_creation_num;
let addr = signer::address_of(creator);
let guid = guid::GUID {
id: guid::ID {
creation_num,
addr,
}
};
let bytes_spec = bcs::to_bytes(guid);
let bytes = concat(bytes_spec, vec<u8>(OBJECT_FROM_GUID_ADDRESS_SCHEME));
let hash_bytes = hash::sha3_256(bytes);
let obj_addr = from_bcs::deserialize<address>(hash_bytes);
aborts_if exists<ObjectCore>(obj_addr);
aborts_if !from_bcs::deserializable<address>(hash_bytes);
ensures global<ObjectCore>(addr).guid_creation_num == old(global<ObjectCore>(addr)).guid_creation_num + 1;
ensures exists<ObjectCore>(obj_addr);
ensures global<ObjectCore>(obj_addr) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: addr,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: obj_addr,
}
}
}
};
ensures result == ConstructorRef { self: obj_addr, can_delete: true };

create_object_from_guid

fun create_object_from_guid(creator_address: address, guid: guid::GUID): object::ConstructorRef
let bytes_spec = bcs::to_bytes(guid);
let bytes = concat(bytes_spec, vec<u8>(OBJECT_FROM_GUID_ADDRESS_SCHEME));
let hash_bytes = hash::sha3_256(bytes);
let obj_addr = from_bcs::deserialize<address>(hash_bytes);
aborts_if exists<ObjectCore>(obj_addr);
aborts_if !from_bcs::deserializable<address>(hash_bytes);
ensures exists<ObjectCore>(obj_addr);
ensures global<ObjectCore>(obj_addr) == ObjectCore {
guid_creation_num: INIT_GUID_CREATION_NUM + 1,
owner: creator_address,
allow_ungated_transfer: true,
transfer_events: event::EventHandle {
counter: 0,
guid: guid::GUID {
id: guid::ID {
creation_num: INIT_GUID_CREATION_NUM,
addr: obj_addr,
}
}
}
};
ensures result == ConstructorRef { self: obj_addr, can_delete: true };

create_object_internal

fun create_object_internal(creator_address: address, object: address, can_delete: bool): object::ConstructorRef
// This enforces high-level requirement 1:
aborts_if exists<ObjectCore>(object);
ensures exists<ObjectCore>(object);
ensures global<ObjectCore>(object).guid_creation_num == INIT_GUID_CREATION_NUM + 1;
ensures result == ConstructorRef { self: object, can_delete };

generate_delete_ref

public fun generate_delete_ref(ref: &object::ConstructorRef): object::DeleteRef
aborts_if !ref.can_delete;
ensures result == DeleteRef { self: ref.self };

generate_transfer_ref

public fun generate_transfer_ref(ref: &object::ConstructorRef): object::TransferRef
aborts_if exists<Untransferable>(ref.self);
ensures result == TransferRef {
self: ref.self,
};

object_from_constructor_ref

public fun object_from_constructor_ref<T: key>(ref: &object::ConstructorRef): object::Object<T>
aborts_if !exists<ObjectCore>(ref.self);
aborts_if !spec_exists_at<T>(ref.self);
ensures result == Object<T> { inner: ref.self };

create_guid

public fun create_guid(object: &signer): guid::GUID
aborts_if !exists<ObjectCore>(signer::address_of(object));
let object_data = global<ObjectCore>(signer::address_of(object));
aborts_if object_data.guid_creation_num + 1 > MAX_U64;
ensures result == guid::GUID {
id: guid::ID {
creation_num: object_data.guid_creation_num,
addr: signer::address_of(object)
}
};

new_event_handle

public fun new_event_handle<T: drop, store>(object: &signer): event::EventHandle<T>
aborts_if !exists<ObjectCore>(signer::address_of(object));
let object_data = global<ObjectCore>(signer::address_of(object));
aborts_if object_data.guid_creation_num + 1 > MAX_U64;
let guid = guid::GUID {
id: guid::ID {
creation_num: object_data.guid_creation_num,
addr: signer::address_of(object)
}
};
ensures result == event::EventHandle<T> {
counter: 0,
guid,
};

object_from_delete_ref

public fun object_from_delete_ref<T: key>(ref: &object::DeleteRef): object::Object<T>
aborts_if !exists<ObjectCore>(ref.self);
aborts_if !spec_exists_at<T>(ref.self);
ensures result == Object<T> { inner: ref.self };

delete

public fun delete(ref: object::DeleteRef)
aborts_if !exists<ObjectCore>(ref.self);
ensures !exists<ObjectCore>(ref.self);

disable_ungated_transfer

public fun disable_ungated_transfer(ref: &object::TransferRef)
aborts_if !exists<ObjectCore>(ref.self);
ensures global<ObjectCore>(ref.self).allow_ungated_transfer == false;

set_untransferable

public fun set_untransferable(ref: &object::ConstructorRef)
aborts_if !exists<ObjectCore>(ref.self);
aborts_if exists<Untransferable>(ref.self);
ensures exists<Untransferable>(ref.self);
ensures global<ObjectCore>(ref.self).allow_ungated_transfer == false;

enable_ungated_transfer

public fun enable_ungated_transfer(ref: &object::TransferRef)
aborts_if exists<Untransferable>(ref.self);
aborts_if !exists<ObjectCore>(ref.self);
ensures global<ObjectCore>(ref.self).allow_ungated_transfer == true;

generate_linear_transfer_ref

public fun generate_linear_transfer_ref(ref: &object::TransferRef): object::LinearTransferRef
aborts_if exists<Untransferable>(ref.self);
aborts_if !exists<ObjectCore>(ref.self);
let owner = global<ObjectCore>(ref.self).owner;
ensures result == LinearTransferRef {
self: ref.self,
owner,
};

transfer_with_ref

public fun transfer_with_ref(ref: object::LinearTransferRef, to: address)
aborts_if exists<Untransferable>(ref.self);
let object = global<ObjectCore>(ref.self);
aborts_if !exists<ObjectCore>(ref.self);
// This enforces high-level requirement 5:
aborts_if object.owner != ref.owner;
ensures global<ObjectCore>(ref.self).owner == to;

transfer_call

public entry fun transfer_call(owner: &signer, object: address, to: address)
pragma aborts_if_is_partial;
let owner_address = signer::address_of(owner);
aborts_if !exists<ObjectCore>(object);
aborts_if !global<ObjectCore>(object).allow_ungated_transfer;

transfer

public entry fun transfer<T: key>(owner: &signer, object: object::Object<T>, to: address)
pragma aborts_if_is_partial;
let owner_address = signer::address_of(owner);
let object_address = object.inner;
aborts_if !exists<ObjectCore>(object_address);
aborts_if !global<ObjectCore>(object_address).allow_ungated_transfer;

transfer_raw

public fun transfer_raw(owner: &signer, object: address, to: address)
pragma aborts_if_is_partial;
let owner_address = signer::address_of(owner);
aborts_if !exists<ObjectCore>(object);
aborts_if !global<ObjectCore>(object).allow_ungated_transfer;

transfer_to_object

public entry fun transfer_to_object<O: key, T: key>(owner: &signer, object: object::Object<O>, to: object::Object<T>)
pragma aborts_if_is_partial;
let owner_address = signer::address_of(owner);
let object_address = object.inner;
aborts_if !exists<ObjectCore>(object_address);
aborts_if !global<ObjectCore>(object_address).allow_ungated_transfer;

verify_ungated_and_descendant

fun verify_ungated_and_descendant(owner: address, destination: address)
pragma aborts_if_is_partial;
pragma unroll = MAXIMUM_OBJECT_NESTING;
aborts_if !exists<ObjectCore>(destination);
aborts_if !global<ObjectCore>(destination).allow_ungated_transfer;

burn

public entry fun burn<T: key>(owner: &signer, object: object::Object<T>)
pragma aborts_if_is_partial;
let object_address = object.inner;
aborts_if !exists<ObjectCore>(object_address);
aborts_if owner(object) != signer::address_of(owner);
ensures exists<TombStone>(object_address);
ensures is_owner(object, signer::address_of(owner));

unburn

public entry fun unburn<T: key>(original_owner: &signer, object: object::Object<T>)
pragma aborts_if_is_partial;
let object_address = object.inner;
aborts_if !exists<ObjectCore>(object_address);
aborts_if !is_burnt(object);
let tomb_stone = borrow_global<TombStone>(object_address);
let original_owner_address = signer::address_of(original_owner);
let object_current_owner = borrow_global<ObjectCore>(object_address).owner;
aborts_if object_current_owner != original_owner_address && tomb_stone.original_owner != original_owner_address;

ungated_transfer_allowed

public fun ungated_transfer_allowed<T: key>(object: object::Object<T>): bool
aborts_if !exists<ObjectCore>(object.inner);
ensures result == global<ObjectCore>(object.inner).allow_ungated_transfer;

owner

#[view]
public fun owner<T: key>(object: object::Object<T>): address
aborts_if !exists<ObjectCore>(object.inner);
ensures result == global<ObjectCore>(object.inner).owner;

is_owner

#[view]
public fun is_owner<T: key>(object: object::Object<T>, owner: address): bool
aborts_if !exists<ObjectCore>(object.inner);
ensures result == (global<ObjectCore>(object.inner).owner == owner);

owns

#[view]
public fun owns<T: key>(object: object::Object<T>, owner: address): bool
pragma aborts_if_is_partial;
let current_address_0 = object.inner;
let object_0 = global<ObjectCore>(current_address_0);
let current_address = object_0.owner;
aborts_if object.inner != owner && !exists<ObjectCore>(object.inner);
ensures current_address_0 == owner ==> result == true;

root_owner

#[view]
public fun root_owner<T: key>(object: object::Object<T>): address
pragma aborts_if_is_partial;
fun spec_create_object_address(source: address, seed: vector<u8>): address;
fun spec_create_user_derived_object_address(source: address, derive_from: address): address;
fun spec_create_guid_object_address(source: address, creation_num: u64): address;

grant_permission

public fun grant_permission<T>(master: &signer, permissioned_signer: &signer, object: object::Object<T>)
pragma aborts_if_is_partial;
aborts_if !permissioned_signer::spec_is_permissioned_signer(permissioned_signer);
aborts_if permissioned_signer::spec_is_permissioned_signer(master);
aborts_if signer::address_of(master) != signer::address_of(permissioned_signer);
fun spec_exists_at<T: key>(object: address): bool;