diff --git a/aptos-move/framework/aptos-token-objects/doc/token.md b/aptos-move/framework/aptos-token-objects/doc/token.md index 17ae275c695..fcd9d5c86fe 100644 --- a/aptos-move/framework/aptos-token-objects/doc/token.md +++ b/aptos-move/framework/aptos-token-objects/doc/token.md @@ -1645,6 +1645,13 @@ as that would prohibit transactions to be executed in parallel. ## Function `royalty` +The royalty published on the token, or, when the token has none of its own, the royalty of +the collection it was minted into. + +The collection is read from the object reference recorded on the token at mint time. It must +never be re-derived from creator + collection_name: collection::set_name moves the name +out from under that derivation, which then resolves to a different collection of the same +creator - paying out its royalty instead - or to no object at all.
#[view]
@@ -1658,15 +1665,11 @@ as that would prohibit transactions to be executed in parallel.
 
 
 
public fun royalty<T: key>(token: Object<T>): Option<Royalty> acquires Token {
-    borrow(&token);
+    let collection = borrow(&token).collection;
     let royalty = royalty::get(token);
     if (royalty.is_some()) {
         royalty
     } else {
-        let creator = creator(token);
-        let collection_name = collection_name(token);
-        let collection_address = collection::create_collection_address(&creator, &collection_name);
-        let collection = object::address_to_object<collection::Collection>(collection_address);
         royalty::get(collection)
     }
 }
diff --git a/aptos-move/framework/aptos-token-objects/sources/token.move b/aptos-move/framework/aptos-token-objects/sources/token.move
index 0c4dc68141a..08ed4a66e1b 100644
--- a/aptos-move/framework/aptos-token-objects/sources/token.move
+++ b/aptos-move/framework/aptos-token-objects/sources/token.move
@@ -682,16 +682,14 @@ module aptos_token_objects::token {
     }
 
     #[view]
+    /// The royalty published on the token, or, when the token has none of its own, the royalty of
+    /// the collection it was minted into.
     public fun royalty(token: Object): Option acquires Token {
-        borrow(&token);
+        let collection = borrow(&token).collection;
         let royalty = royalty::get(token);
         if (royalty.is_some()) {
             royalty
         } else {
-            let creator = creator(token);
-            let collection_name = collection_name(token);
-            let collection_address = collection::create_collection_address(&creator, &collection_name);
-            let collection = object::address_to_object(collection_address);
             royalty::get(collection)
         }
     }
@@ -1296,6 +1294,145 @@ module aptos_token_objects::token {
         assert!(collection::count(collection) == option::some(2), 0);
     }
 
+    #[test(creator = @0x123)]
+    /// A token without its own royalty reads the royalty of the collection it was minted into, and
+    /// keeps reading it after the collection is renamed. Resolving the collection by re-deriving
+    /// `creator + collection_name` breaks here: nothing is published at the address derived from
+    /// the new name.
+    fun test_collection_royalty_fallback_survives_rename(creator: &signer) acquires Token {
+        let creator_address = signer::address_of(creator);
+        let expected_royalty = royalty::create(5, 100, creator_address);
+
+        let collection_ref = create_collection_with_royalty_helper(
+            creator,
+            string::utf8(b"collection name"),
+            expected_royalty,
+        );
+        let collection = object::object_from_constructor_ref(&collection_ref);
+        let mutator_ref = collection::generate_mutator_ref(&collection_ref);
+
+        let token_ref = create_token_without_royalty_helper(creator, collection, string::utf8(b"token name"));
+        let token = object::object_from_constructor_ref(&token_ref);
+        assert!(royalty(token) == option::some(expected_royalty), 0);
+
+        collection::set_name(&mutator_ref, string::utf8(b"renamed collection name"));
+        assert!(royalty(token) == option::some(expected_royalty), 1);
+    }
+
+    #[test(creator = @0x123)]
+    /// Renaming a collection onto the name of another collection by the same creator must not
+    /// repoint the renamed collection's tokens at the other collection's royalty. After the rename,
+    /// `create_collection_address(creator, collection_name(token))` is exactly the other
+    /// collection's address, so resolving the collection by name pays out the wrong royalty - and
+    /// does so even when the original collection's royalty was published as immutable.
+    fun test_collection_royalty_not_repointed_by_name_collision(creator: &signer) acquires Token {
+        let creator_address = signer::address_of(creator);
+        let original_name = string::utf8(b"collection name");
+        let other_collection_name = string::utf8(b"other collection name");
+
+        let expected_royalty = royalty::create(5, 100, creator_address);
+        let collection_ref = create_collection_with_royalty_helper(creator, original_name, expected_royalty);
+        let collection = object::object_from_constructor_ref(&collection_ref);
+        let mutator_ref = collection::generate_mutator_ref(&collection_ref);
+
+        // A second collection by the same creator, paying a different royalty to a different payee.
+        let other_royalty = royalty::create(99, 100, @0xbad);
+        create_collection_with_royalty_helper(creator, other_collection_name, other_royalty);
+
+        let token_ref = create_token_without_royalty_helper(creator, collection, string::utf8(b"token name"));
+        let token = object::object_from_constructor_ref(&token_ref);
+
+        collection::set_name(&mutator_ref, other_collection_name);
+
+        // The name-derived address now points at the other collection rather than at this token's.
+        let derived_address = collection::create_collection_address(&creator_address, &collection_name(token));
+        assert!(derived_address != object::object_address(&collection), 0);
+        assert!(royalty::get(object::address_to_object(derived_address)) == option::some(other_royalty), 1);
+
+        let actual_royalty = royalty(token).destroy_some();
+        assert!(actual_royalty == expected_royalty, 2);
+        assert!(royalty::payee_address(&actual_royalty) == creator_address, 3);
+        assert!(royalty::numerator(&actual_royalty) == 5, 4);
+    }
+
+    #[test(creator = @0x123)]
+    /// A royalty published on the token itself wins over the collection's, before and after a rename.
+    fun test_token_royalty_takes_precedence_over_collection(creator: &signer) acquires Token {
+        let creator_address = signer::address_of(creator);
+        let collection_royalty = royalty::create(5, 100, creator_address);
+        let token_royalty = royalty::create(10, 100, @0xa11ce);
+
+        let collection_ref = create_collection_with_royalty_helper(
+            creator,
+            string::utf8(b"collection name"),
+            collection_royalty,
+        );
+        let collection = object::object_from_constructor_ref(&collection_ref);
+        let mutator_ref = collection::generate_mutator_ref(&collection_ref);
+
+        let token_ref = create_named_token_object(
+            creator,
+            collection,
+            string::utf8(b"token description"),
+            string::utf8(b"token name"),
+            option::some(token_royalty),
+            string::utf8(b"uri"),
+        );
+        let token = object::object_from_constructor_ref(&token_ref);
+        assert!(royalty(token) == option::some(token_royalty), 0);
+
+        collection::set_name(&mutator_ref, string::utf8(b"renamed collection name"));
+        assert!(royalty(token) == option::some(token_royalty), 1);
+    }
+
+    #[test(creator = @0x123)]
+    /// Neither the token nor its collection carries a royalty: the lookup reports none rather than
+    /// aborting, both before and after a rename.
+    fun test_royalty_absent_survives_rename(creator: &signer) acquires Token {
+        let collection_ref = create_fixed_collection(creator, string::utf8(b"collection name"), 5);
+        let collection = object::object_from_constructor_ref(&collection_ref);
+        let mutator_ref = collection::generate_mutator_ref(&collection_ref);
+
+        let token_ref = create_token_without_royalty_helper(creator, collection, string::utf8(b"token name"));
+        let token = object::object_from_constructor_ref(&token_ref);
+        assert!(royalty(token).is_none(), 0);
+
+        collection::set_name(&mutator_ref, string::utf8(b"renamed collection name"));
+        assert!(royalty(token).is_none(), 1);
+    }
+
+    #[test_only]
+    fun create_collection_with_royalty_helper(
+        creator: &signer,
+        collection_name: String,
+        royalty: Royalty,
+    ): ConstructorRef {
+        collection::create_fixed_collection(
+            creator,
+            string::utf8(b"collection description"),
+            5,
+            collection_name,
+            option::some(royalty),
+            string::utf8(b"collection uri"),
+        )
+    }
+
+    #[test_only]
+    fun create_token_without_royalty_helper(
+        creator: &signer,
+        collection: Object,
+        token_name: String,
+    ): ConstructorRef {
+        create_named_token_object(
+            creator,
+            collection,
+            string::utf8(b"token description"),
+            token_name,
+            option::none(),
+            string::utf8(b"uri"),
+        )
+    }
+
     #[test_only]
     fun create_collection_helper(creator: &signer, collection_name: String, max_supply: u64): ExtendRef {
         let constructor_ref = create_fixed_collection(creator, collection_name, max_supply);