Skip to content

Commit

Permalink
AIP-109 Safe burn of unwanted soulbound objects (#15095)
Browse files Browse the repository at this point in the history
* [framework] Add safe burn without transfer

The issue with AIP-99 was it removed the tombstoning
in addition to the transfer.  The transfer to a burn
address as an owner was the main issue.

Indexers will need to identify the TombStone and hide
them from users accordingly.

* fixup: abort on already burnt
  • Loading branch information
gregnazario authored Feb 4, 2025
1 parent 8793674 commit a88a081
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 73 deletions.
72 changes: 47 additions & 25 deletions aptos-move/framework/aptos-framework/doc/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,16 +637,6 @@ generate_unique_address uses this for domain separation within its native implem



<a id="0x1_object_EBURN_NOT_ALLOWED"></a>

Objects cannot be burnt


<pre><code><b>const</b> <a href="object.md#0x1_object_EBURN_NOT_ALLOWED">EBURN_NOT_ALLOWED</a>: u64 = 10;
</code></pre>



<a id="0x1_object_ECANNOT_DELETE"></a>

The object does not allow for deletion
Expand Down Expand Up @@ -687,6 +677,16 @@ The object does not have ungated transfers enabled



<a id="0x1_object_EOBJECT_ALREADY_BURNT"></a>

Cannot burn an object that is already burnt.


<pre><code><b>const</b> <a href="object.md#0x1_object_EOBJECT_ALREADY_BURNT">EOBJECT_ALREADY_BURNT</a>: u64 = 10;
</code></pre>



<a id="0x1_object_EOBJECT_DOES_NOT_EXIST"></a>

An object does not exist at this address
Expand Down Expand Up @@ -1720,7 +1720,7 @@ Removes from the specified Object from global storage.
} = object_core;

<b>if</b> (<b>exists</b>&lt;<a href="object.md#0x1_object_Untransferable">Untransferable</a>&gt;(ref.self)) {
<b>let</b> <a href="object.md#0x1_object_Untransferable">Untransferable</a> {} = <b>move_from</b>&lt;<a href="object.md#0x1_object_Untransferable">Untransferable</a>&gt;(ref.self);
<b>let</b> <a href="object.md#0x1_object_Untransferable">Untransferable</a> {} = <b>move_from</b>&lt;<a href="object.md#0x1_object_Untransferable">Untransferable</a>&gt;(ref.self);
};

<a href="event.md#0x1_event_destroy_handle">event::destroy_handle</a>(transfer_events);
Expand Down Expand Up @@ -2179,13 +2179,13 @@ objects may have cyclic dependencies.

## Function `burn`

Previously allowed to burn objects, has now been disabled. Objects can still be unburnt.

Please use the test only [<code>object::burn_object</code>] for testing with previously burned objects.
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 [<code>object::burn_object_with_transfer</code>] for testing with previously burned objects.


<pre><code>#[deprecated]
<b>public</b> entry <b>fun</b> <a href="object.md#0x1_object_burn">burn</a>&lt;T: key&gt;(_owner: &<a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer">signer</a>, _object: <a href="object.md#0x1_object_Object">object::Object</a>&lt;T&gt;)
<pre><code><b>public</b> entry <b>fun</b> <a href="object.md#0x1_object_burn">burn</a>&lt;T: key&gt;(owner: &<a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer">signer</a>, <a href="object.md#0x1_object">object</a>: <a href="object.md#0x1_object_Object">object::Object</a>&lt;T&gt;)
</code></pre>


Expand All @@ -2194,8 +2194,12 @@ Please use the test only [<code>object::burn_object</code>] for testing with pre
<summary>Implementation</summary>


<pre><code><b>public</b> entry <b>fun</b> <a href="object.md#0x1_object_burn">burn</a>&lt;T: key&gt;(_owner: &<a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer">signer</a>, _object: <a href="object.md#0x1_object_Object">Object</a>&lt;T&gt;) {
<b>abort</b> <a href="../../aptos-stdlib/../move-stdlib/doc/error.md#0x1_error_permission_denied">error::permission_denied</a>(<a href="object.md#0x1_object_EBURN_NOT_ALLOWED">EBURN_NOT_ALLOWED</a>)
<pre><code><b>public</b> entry <b>fun</b> <a href="object.md#0x1_object_burn">burn</a>&lt;T: key&gt;(owner: &<a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer">signer</a>, <a href="object.md#0x1_object">object</a>: <a href="object.md#0x1_object_Object">Object</a>&lt;T&gt;) <b>acquires</b> <a href="object.md#0x1_object_ObjectCore">ObjectCore</a> {
<b>let</b> original_owner = <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(owner);
<b>assert</b>!(<a href="object.md#0x1_object_is_owner">is_owner</a>(<a href="object.md#0x1_object">object</a>, original_owner), <a href="../../aptos-stdlib/../move-stdlib/doc/error.md#0x1_error_permission_denied">error::permission_denied</a>(<a href="object.md#0x1_object_ENOT_OBJECT_OWNER">ENOT_OBJECT_OWNER</a>));
<b>let</b> object_addr = <a href="object.md#0x1_object">object</a>.inner;
<b>assert</b>!(!<b>exists</b>&lt;<a href="object.md#0x1_object_TombStone">TombStone</a>&gt;(object_addr), <a href="object.md#0x1_object_EOBJECT_ALREADY_BURNT">EOBJECT_ALREADY_BURNT</a>);
<b>move_to</b>(&<a href="create_signer.md#0x1_create_signer">create_signer</a>(object_addr), <a href="object.md#0x1_object_TombStone">TombStone</a> { original_owner });
}
</code></pre>

Expand Down Expand Up @@ -2230,9 +2234,21 @@ Allow origin owners to reclaim any objects they previous burnt.
<a href="../../aptos-stdlib/../move-stdlib/doc/error.md#0x1_error_permission_denied">error::permission_denied</a>(<a href="object.md#0x1_object_EOBJECT_NOT_TRANSFERRABLE">EOBJECT_NOT_TRANSFERRABLE</a>)
);

<b>let</b> <a href="object.md#0x1_object_TombStone">TombStone</a> { original_owner: original_owner_addr } = <b>move_from</b>&lt;<a href="object.md#0x1_object_TombStone">TombStone</a>&gt;(object_addr);
<b>assert</b>!(original_owner_addr == <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(original_owner), <a href="../../aptos-stdlib/../move-stdlib/doc/error.md#0x1_error_permission_denied">error::permission_denied</a>(<a href="object.md#0x1_object_ENOT_OBJECT_OWNER">ENOT_OBJECT_OWNER</a>));
<a href="object.md#0x1_object_transfer_raw_inner">transfer_raw_inner</a>(object_addr, original_owner_addr);
// The new owner of the <a href="object.md#0x1_object">object</a> can always unburn it, but <b>if</b> it's the burn <b>address</b>, we go <b>to</b> the <b>old</b> functionality
<b>let</b> object_core = <b>borrow_global</b>&lt;<a href="object.md#0x1_object_ObjectCore">ObjectCore</a>&gt;(object_addr);
<b>if</b> (object_core.owner == <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(original_owner)) {
<b>let</b> <a href="object.md#0x1_object_TombStone">TombStone</a> { original_owner: _ } = <b>move_from</b>&lt;<a href="object.md#0x1_object_TombStone">TombStone</a>&gt;(object_addr);
} <b>else</b> <b>if</b> (object_core.owner == <a href="object.md#0x1_object_BURN_ADDRESS">BURN_ADDRESS</a>) {
// The <b>old</b> functionality
<b>let</b> <a href="object.md#0x1_object_TombStone">TombStone</a> { original_owner: original_owner_addr } = <b>move_from</b>&lt;<a href="object.md#0x1_object_TombStone">TombStone</a>&gt;(object_addr);
<b>assert</b>!(
original_owner_addr == <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(original_owner),
<a href="../../aptos-stdlib/../move-stdlib/doc/error.md#0x1_error_permission_denied">error::permission_denied</a>(<a href="object.md#0x1_object_ENOT_OBJECT_OWNER">ENOT_OBJECT_OWNER</a>)
);
<a href="object.md#0x1_object_transfer_raw_inner">transfer_raw_inner</a>(object_addr, original_owner_addr);
} <b>else</b> {
<b>abort</b> <a href="../../aptos-stdlib/../move-stdlib/doc/error.md#0x1_error_permission_denied">error::permission_denied</a>(<a href="object.md#0x1_object_ENOT_OBJECT_OWNER">ENOT_OBJECT_OWNER</a>);
};
}
</code></pre>

Expand Down Expand Up @@ -3351,14 +3367,18 @@ Grant a transfer permission to the permissioned signer using TransferRef.
### Function `burn`


<pre><code>#[deprecated]
<b>public</b> entry <b>fun</b> <a href="object.md#0x1_object_burn">burn</a>&lt;T: key&gt;(_owner: &<a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer">signer</a>, _object: <a href="object.md#0x1_object_Object">object::Object</a>&lt;T&gt;)
<pre><code><b>public</b> entry <b>fun</b> <a href="object.md#0x1_object_burn">burn</a>&lt;T: key&gt;(owner: &<a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer">signer</a>, <a href="object.md#0x1_object">object</a>: <a href="object.md#0x1_object_Object">object::Object</a>&lt;T&gt;)
</code></pre>




<pre><code><b>aborts_if</b> <b>true</b>;
<pre><code><b>pragma</b> aborts_if_is_partial;
<b>let</b> object_address = <a href="object.md#0x1_object">object</a>.inner;
<b>aborts_if</b> !<b>exists</b>&lt;<a href="object.md#0x1_object_ObjectCore">ObjectCore</a>&gt;(object_address);
<b>aborts_if</b> <a href="object.md#0x1_object_owner">owner</a>(<a href="object.md#0x1_object">object</a>) != <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(owner);
<b>ensures</b> <b>exists</b>&lt;<a href="object.md#0x1_object_TombStone">TombStone</a>&gt;(object_address);
<b>ensures</b> <a href="object.md#0x1_object_is_owner">is_owner</a>(<a href="object.md#0x1_object">object</a>, <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(owner));
</code></pre>


Expand All @@ -3379,7 +3399,9 @@ Grant a transfer permission to the permissioned signer using TransferRef.
<b>aborts_if</b> !<b>exists</b>&lt;<a href="object.md#0x1_object_ObjectCore">ObjectCore</a>&gt;(object_address);
<b>aborts_if</b> !<a href="object.md#0x1_object_is_burnt">is_burnt</a>(<a href="object.md#0x1_object">object</a>);
<b>let</b> tomb_stone = <b>borrow_global</b>&lt;<a href="object.md#0x1_object_TombStone">TombStone</a>&gt;(object_address);
<b>aborts_if</b> tomb_stone.original_owner != <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(original_owner);
<b>let</b> original_owner_address = <a href="../../aptos-stdlib/../move-stdlib/doc/signer.md#0x1_signer_address_of">signer::address_of</a>(original_owner);
<b>let</b> object_current_owner = <b>borrow_global</b>&lt;<a href="object.md#0x1_object_ObjectCore">ObjectCore</a>&gt;(object_address).owner;
<b>aborts_if</b> object_current_owner != original_owner_address && tomb_stone.original_owner != original_owner_address;
</code></pre>


Expand Down
91 changes: 67 additions & 24 deletions aptos-move/framework/aptos-framework/sources/object.move
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ module aptos_framework::object {
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;
/// Objects cannot be burnt
const EBURN_NOT_ALLOWED: u64 = 10;
/// Cannot burn an object that is already burnt.
const EOBJECT_ALREADY_BURNT: u64 = 10;

/// Explicitly separate the GUID space between Object and Account to prevent accidental overlap.
const INIT_GUID_CREATION_NUM: u64 = 0x4000000000000;
Expand Down Expand Up @@ -430,7 +430,7 @@ module aptos_framework::object {
} = object_core;

if (exists<Untransferable>(ref.self)) {
let Untransferable {} = move_from<Untransferable>(ref.self);
let Untransferable {} = move_from<Untransferable>(ref.self);
};

event::destroy_handle(transfer_events);
Expand Down Expand Up @@ -624,12 +624,16 @@ module aptos_framework::object {
};
}

#[deprecated]
/// Previously allowed to burn objects, has now been disabled. Objects can still be unburnt.
///
/// Please use the test only [`object::burn_object`] for testing with previously burned objects.
public entry fun burn<T: key>(_owner: &signer, _object: Object<T>) {
abort error::permission_denied(EBURN_NOT_ALLOWED)
/// 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<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 });
}

/// Allow origin owners to reclaim any objects they previous burnt.
Expand All @@ -644,9 +648,21 @@ module aptos_framework::object {
error::permission_denied(EOBJECT_NOT_TRANSFERRABLE)
);

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);
// 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);
};
}

/// Accessors
Expand Down Expand Up @@ -751,7 +767,7 @@ module aptos_framework::object {
/// Forcefully transfer an unwanted object to BURN_ADDRESS, ignoring whether ungated_transfer is allowed.
/// 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.
public fun burn_object<T: key>(owner: &signer, object: Object<T>) acquires ObjectCore {
public fun burn_object_with_transfer<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;
Expand Down Expand Up @@ -870,11 +886,21 @@ module aptos_framework::object {
transfer_with_ref(linear_transfer_ref_bad, @0x789);
}

#[test(creator = @0x123)]
#[expected_failure(abort_code = 0x10008, location = Self)]
fun test_cannot_unburn_legacy_after_transfer_with_ref(creator: &signer) acquires ObjectCore, TombStone {
let (hero_constructor, hero) = create_hero(creator);
burn_object_with_transfer(creator, hero);
let transfer_ref = generate_transfer_ref(&hero_constructor);
transfer_with_ref(generate_linear_transfer_ref(&transfer_ref), @0x456);
unburn(creator, hero);
}

#[test(creator = @0x123)]
#[expected_failure(abort_code = 0x10008, location = Self)]
fun test_cannot_unburn_after_transfer_with_ref(creator: &signer) acquires ObjectCore, TombStone {
let (hero_constructor, hero) = create_hero(creator);
burn_object(creator, hero);
burn(creator, hero);
let transfer_ref = generate_transfer_ref(&hero_constructor);
transfer_with_ref(generate_linear_transfer_ref(&transfer_ref), @0x456);
unburn(creator, hero);
Expand Down Expand Up @@ -930,7 +956,29 @@ module aptos_framework::object {
disable_ungated_transfer(&transfer_ref);

// Owner should be able to burn, despite ungated transfer disallowed.
burn_object(creator, hero);
burn(creator, hero);
assert!(owner(hero) == signer::address_of(creator), 0);
assert!(!ungated_transfer_allowed(hero), 0);
assert!(exists<TombStone>(object_address(&hero)), 0);

// Owner should be able to reclaim.
unburn(creator, hero);
assert!(owner(hero) == signer::address_of(creator), 0);
// Object still frozen.
assert!(!ungated_transfer_allowed(hero), 0);
// Tombstone gone
assert!(!exists<TombStone>(object_address(&hero)), 0);
}

#[test(creator = @0x123)]
fun test_burn_and_unburn_old(creator: &signer) acquires ObjectCore, TombStone {
let (hero_constructor, hero) = create_hero(creator);
// Freeze the object.
let transfer_ref = generate_transfer_ref(&hero_constructor);
disable_ungated_transfer(&transfer_ref);

// Owner should be able to burn, despite ungated transfer disallowed.
burn_object_with_transfer(creator, hero);
assert!(owner(hero) == BURN_ADDRESS, 0);
assert!(!ungated_transfer_allowed(hero), 0);

Expand All @@ -951,7 +999,7 @@ module aptos_framework::object {
// Owner should be not be able to burn weapon directly.
assert!(owner(weapon) == object_address(&hero), 0);
assert!(owns(weapon, signer::address_of(creator)), 0);
burn_object(creator, weapon);
burn(creator, weapon);
}

#[test(creator = @0x123)]
Expand All @@ -961,13 +1009,6 @@ module aptos_framework::object {
unburn(creator, hero);
}

#[test(creator = @0x123)]
#[expected_failure(abort_code = 0x5000A, location = Self)]
fun test_burn_should_fail(creator: &signer) acquires ObjectCore {
let (_, hero) = create_hero(creator);
burn(creator, hero);
}

#[test_only]
fun create_simple_object(creator: &signer, seed: vector<u8>): Object<ObjectCore> {
object_from_constructor_ref<ObjectCore>(&create_named_object(creator, seed))
Expand Down Expand Up @@ -1122,7 +1163,9 @@ module aptos_framework::object {

#[test(creator = @0x123)]
#[expected_failure(abort_code = 327689, location = Self)]
fun test_untransferable_indirect_ownership_with_linear_transfer_ref(creator: &signer) acquires ObjectCore, TombStone {
fun test_untransferable_indirect_ownership_with_linear_transfer_ref(
creator: &signer
) acquires ObjectCore, TombStone {
let (_, hero) = create_hero(creator);
let (weapon_constructor_ref, weapon) = create_weapon(creator);
transfer_to_object(creator, weapon, hero);
Expand Down
Loading

0 comments on commit a88a081

Please sign in to comment.