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 TransferRef
s 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
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
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
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
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. | Requirement | Criticality | Implementation | Enforcement |
---|---|---|---|---|
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;