[NFTs] Rework permissions model (#13482)

* Disallow admin to transfer or burn items he doesn't own

* lock_collection should be accessible by collection's owner only

* Allow admin to access lock_item_properties()

* Fix do_lock_item_properties

* Move update_mint_settings() to Issuer

* Rename check_owner to check_origin

* Typo

* Make admin to be in charge of managing the metadata

* Make admin the main attributes manager

* offchain mint should be signed by Issuer

* Remove the special case when the Issuer calls the mint() function

* Rework burn and destroy methods

* Return back item_metadatas

* Don't repatriate the deposit on transfer

* A bit more tests

* One more test

* Add migration

* Chore

* Clippy

* Rename to owned_item

* Address comments

* Replace .filter_map with .find_map

* Improve version validation in pre_upgrade()

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_nfts

---------

Co-authored-by: parity-processbot <>
This commit is contained in:
Jegor Sidorenko
2023-03-13 10:25:46 +02:00
committed by GitHub
parent 66f3d9e237
commit f6b9e056ae
15 changed files with 968 additions and 802 deletions
+107 -38
View File
@@ -215,21 +215,44 @@ fn lifecycle_should_work() {
assert_eq!(items(), vec![(account(1), 0, 70), (account(10), 0, 42), (account(20), 0, 69)]);
assert_eq!(Collection::<Test>::get(0).unwrap().items, 3);
assert_eq!(Collection::<Test>::get(0).unwrap().item_metadatas, 0);
assert_eq!(Collection::<Test>::get(0).unwrap().item_configs, 3);
assert_eq!(Balances::reserved_balance(&account(2)), 0);
assert_eq!(Balances::reserved_balance(&account(1)), 8);
assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 70, account(2)));
assert_eq!(Balances::reserved_balance(&account(2)), 1);
assert_eq!(Balances::reserved_balance(&account(1)), 8);
assert_eq!(Balances::reserved_balance(&account(2)), 0);
assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![42, 42]));
assert_eq!(Balances::reserved_balance(&account(1)), 10);
assert_eq!(Balances::reserved_balance(&account(1)), 11);
assert!(ItemMetadataOf::<Test>::contains_key(0, 42));
assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 69, bvec![69, 69]));
assert_eq!(Balances::reserved_balance(&account(1)), 13);
assert_eq!(Balances::reserved_balance(&account(1)), 14);
assert!(ItemMetadataOf::<Test>::contains_key(0, 69));
assert!(ItemConfigOf::<Test>::contains_key(0, 69));
let w = Nfts::get_destroy_witness(&0).unwrap();
assert_eq!(w.item_metadatas, 2);
assert_eq!(w.item_configs, 3);
assert_noop!(
Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w),
Error::<Test>::CollectionNotEmpty
);
assert_ok!(Nfts::set_attribute(
RuntimeOrigin::signed(account(1)),
0,
Some(69),
AttributeNamespace::CollectionOwner,
bvec![0],
bvec![0],
));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(10)), 0, 42));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(20)), 0, 69));
assert_ok!(Nfts::burn(RuntimeOrigin::root(), 0, 70));
let w = Nfts::get_destroy_witness(&0).unwrap();
assert_eq!(w.items, 3);
assert_eq!(w.item_metadatas, 2);
assert_eq!(w.attributes, 1);
assert_eq!(w.item_metadatas, 0);
assert_eq!(w.item_configs, 0);
assert_ok!(Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w));
assert_eq!(Balances::reserved_balance(&account(1)), 0);
@@ -240,6 +263,8 @@ fn lifecycle_should_work() {
assert!(!CollectionMetadataOf::<Test>::contains_key(0));
assert!(!ItemMetadataOf::<Test>::contains_key(0, 42));
assert!(!ItemMetadataOf::<Test>::contains_key(0, 69));
assert!(!ItemConfigOf::<Test>::contains_key(0, 69));
assert_eq!(attributes(0), vec![]);
assert_eq!(collections(), vec![]);
assert_eq!(items(), vec![]);
});
@@ -256,14 +281,51 @@ fn destroy_with_bad_witness_should_not_work() {
));
let w = Collection::<Test>::get(0).unwrap().destroy_witness();
assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None));
assert_noop!(
Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w),
Nfts::destroy(
RuntimeOrigin::signed(account(1)),
0,
DestroyWitness { item_configs: 1, ..w }
),
Error::<Test>::BadWitness
);
});
}
#[test]
fn destroy_should_work() {
new_test_ext().execute_with(|| {
Balances::make_free_balance_be(&account(1), 100);
assert_ok!(Nfts::create(
RuntimeOrigin::signed(account(1)),
account(1),
collection_config_with_all_settings_enabled()
));
assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(2), None));
assert_noop!(
Nfts::destroy(
RuntimeOrigin::signed(account(1)),
0,
Nfts::get_destroy_witness(&0).unwrap()
),
Error::<Test>::CollectionNotEmpty
);
assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(account(1)), 0, 42));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(2)), 0, 42));
assert_eq!(Collection::<Test>::get(0).unwrap().item_configs, 1);
assert_eq!(ItemConfigOf::<Test>::iter_prefix(0).count() as u32, 1);
assert!(ItemConfigOf::<Test>::contains_key(0, 42));
assert_ok!(Nfts::destroy(
RuntimeOrigin::signed(account(1)),
0,
Nfts::get_destroy_witness(&0).unwrap()
));
assert!(!ItemConfigOf::<Test>::contains_key(0, 42));
assert_eq!(ItemConfigOf::<Test>::iter_prefix(0).count() as u32, 0);
});
}
#[test]
fn mint_should_work() {
new_test_ext().execute_with(|| {
@@ -491,8 +553,9 @@ fn origin_guards_should_work() {
Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 69, account(2), None),
Error::<Test>::NoPermission
);
assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 43, account(2), None));
assert_noop!(
Nfts::burn(RuntimeOrigin::signed(account(2)), 0, 42, None),
Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 43),
Error::<Test>::NoPermission
);
let w = Nfts::get_destroy_witness(&0).unwrap();
@@ -536,13 +599,13 @@ fn transfer_owner_should_work() {
// Mint and set metadata now and make sure that deposit gets transferred back.
assert_ok!(Nfts::set_collection_metadata(
RuntimeOrigin::signed(account(2)),
RuntimeOrigin::signed(account(1)),
0,
bvec![0u8; 20],
));
assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None));
assert_eq!(Balances::reserved_balance(&account(1)), 1);
assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(2)), 0, 42, bvec![0u8; 20]));
assert_ok!(Nfts::set_metadata(RuntimeOrigin::signed(account(1)), 0, 42, bvec![0u8; 20]));
assert_ok!(Nfts::set_accept_ownership(RuntimeOrigin::signed(account(3)), Some(0)));
assert_ok!(Nfts::transfer_ownership(RuntimeOrigin::signed(account(2)), 0, account(3)));
assert_eq!(collections(), vec![(account(3), 0)]);
@@ -552,8 +615,9 @@ fn transfer_owner_should_work() {
assert_eq!(Balances::reserved_balance(&account(3)), 44);
assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 42, account(2)));
assert_eq!(Balances::reserved_balance(&account(1)), 0);
assert_eq!(Balances::reserved_balance(&account(2)), 1);
// reserved_balance of accounts 1 & 2 should be unchanged:
assert_eq!(Balances::reserved_balance(&account(1)), 1);
assert_eq!(Balances::reserved_balance(&account(2)), 0);
// 2's acceptance from before is reset when it became an owner, so it cannot be transferred
// without a fresh acceptance.
@@ -583,8 +647,14 @@ fn set_team_should_work() {
assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(2)), 0, 42, account(2), None));
assert_ok!(Nfts::lock_item_transfer(RuntimeOrigin::signed(account(4)), 0, 42));
assert_ok!(Nfts::unlock_item_transfer(RuntimeOrigin::signed(account(4)), 0, 42));
assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(3)));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 42, None));
assert_noop!(
Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(3)),
Error::<Test>::NoPermission
);
assert_noop!(
Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 42),
Error::<Test>::NoPermission
);
});
}
@@ -594,7 +664,7 @@ fn set_collection_metadata_should_work() {
// Cannot add metadata to unknown item
assert_noop!(
Nfts::set_collection_metadata(RuntimeOrigin::signed(account(1)), 0, bvec![0u8; 20]),
Error::<Test>::NoConfig,
Error::<Test>::NoPermission,
);
assert_ok!(Nfts::force_create(
RuntimeOrigin::root(),
@@ -668,7 +738,7 @@ fn set_collection_metadata_should_work() {
);
assert_noop!(
Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(1)), 1),
Error::<Test>::UnknownCollection
Error::<Test>::NoPermission
);
assert_noop!(
Nfts::clear_collection_metadata(RuntimeOrigin::signed(account(1)), 0),
@@ -743,7 +813,7 @@ fn set_item_metadata_should_work() {
);
assert_noop!(
Nfts::clear_metadata(RuntimeOrigin::signed(account(1)), 1, 42),
Error::<Test>::MetadataNotFound,
Error::<Test>::NoPermission,
);
assert_ok!(Nfts::clear_metadata(RuntimeOrigin::root(), 0, 42));
assert!(!ItemMetadataOf::<Test>::contains_key(0, 42));
@@ -832,6 +902,7 @@ fn set_collection_owner_attributes_should_work() {
);
assert_eq!(Balances::reserved_balance(account(1)), 16);
assert_ok!(Nfts::burn(RuntimeOrigin::root(), 0, 0));
let w = Nfts::get_destroy_witness(&0).unwrap();
assert_ok!(Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w));
assert_eq!(attributes(0), vec![]);
@@ -997,7 +1068,7 @@ fn set_item_owner_attributes_should_work() {
assert_eq!(Balances::reserved_balance(account(3)), 13);
// validate attributes on item deletion
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 0, None));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 0));
assert_eq!(
attributes(0),
vec![
@@ -1317,7 +1388,7 @@ fn preserve_config_for_frozen_items() {
assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 1, account(1), None));
// if the item is not locked/frozen then the config gets deleted on item burn
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 1, Some(account(1))));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 1));
assert!(!ItemConfigOf::<Test>::contains_key(0, 1));
// lock the item and ensure the config stays unchanged
@@ -1329,7 +1400,7 @@ fn preserve_config_for_frozen_items() {
let config = ItemConfigOf::<Test>::get(0, 0).unwrap();
assert_eq!(config, expect_config);
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 0, Some(account(1))));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(1)), 0, 0));
let config = ItemConfigOf::<Test>::get(0, 0).unwrap();
assert_eq!(config, expect_config);
@@ -1405,6 +1476,7 @@ fn force_update_collection_should_work() {
Balances::make_free_balance_be(&account(5), 100);
assert_ok!(Nfts::force_collection_owner(RuntimeOrigin::root(), 0, account(5)));
assert_ok!(Nfts::set_team(RuntimeOrigin::root(), 0, account(2), account(5), account(4)));
assert_eq!(collections(), vec![(account(5), 0)]);
assert_eq!(Balances::reserved_balance(account(1)), 2);
assert_eq!(Balances::reserved_balance(account(5)), 63);
@@ -1475,7 +1547,7 @@ fn burn_works() {
));
assert_noop!(
Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42, Some(account(5))),
Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42),
Error::<Test>::UnknownItem
);
@@ -1496,16 +1568,12 @@ fn burn_works() {
assert_eq!(Balances::reserved_balance(account(1)), 2);
assert_noop!(
Nfts::burn(RuntimeOrigin::signed(account(0)), 0, 42, None),
Nfts::burn(RuntimeOrigin::signed(account(0)), 0, 42),
Error::<Test>::NoPermission
);
assert_noop!(
Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42, Some(account(6))),
Error::<Test>::WrongOwner
);
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42, Some(account(5))));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(3)), 0, 69, Some(account(5))));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 69));
assert_eq!(Balances::reserved_balance(account(1)), 0);
});
}
@@ -1819,21 +1887,21 @@ fn cancel_approval_works_with_admin() {
None
));
assert_noop!(
Nfts::cancel_approval(RuntimeOrigin::signed(account(1)), 1, 42, account(1)),
Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, 42, account(1)),
Error::<Test>::UnknownItem
);
assert_noop!(
Nfts::cancel_approval(RuntimeOrigin::signed(account(1)), 0, 43, account(1)),
Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 43, account(1)),
Error::<Test>::UnknownItem
);
assert_noop!(
Nfts::cancel_approval(RuntimeOrigin::signed(account(1)), 0, 42, account(4)),
Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(4)),
Error::<Test>::NotDelegate
);
assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(1)), 0, 42, account(3)));
assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3)));
assert_noop!(
Nfts::cancel_approval(RuntimeOrigin::signed(account(1)), 0, 42, account(1)),
Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(1)),
Error::<Test>::NotDelegate
);
});
@@ -3013,6 +3081,7 @@ fn add_remove_item_attributes_approval_should_work() {
#[test]
fn pre_signed_mints_should_work() {
new_test_ext().execute_with(|| {
let user_0 = account(0);
let user_1_pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
let user_1_signer = MultiSigner::Sr25519(user_1_pair.public());
let user_1 = user_1_signer.clone().into_account();
@@ -3029,10 +3098,10 @@ fn pre_signed_mints_should_work() {
let user_2 = account(2);
let user_3 = account(3);
Balances::make_free_balance_be(&user_1, 100);
Balances::make_free_balance_be(&user_0, 100);
Balances::make_free_balance_be(&user_2, 100);
assert_ok!(Nfts::create(
RuntimeOrigin::signed(user_1.clone()),
RuntimeOrigin::signed(user_0.clone()),
user_1.clone(),
collection_config_with_all_settings_enabled(),
));
@@ -3069,7 +3138,7 @@ fn pre_signed_mints_should_work() {
assert_eq!(deposit.account, Some(user_2.clone()));
assert_eq!(deposit.amount, 3);
assert_eq!(Balances::free_balance(&user_1), 100 - 2); // 2 - collection deposit
assert_eq!(Balances::free_balance(&user_0), 100 - 2); // 2 - collection deposit
assert_eq!(Balances::free_balance(&user_2), 100 - 1 - 3 - 6); // 1 - item deposit, 3 - metadata, 6 - attributes
assert_noop!(
@@ -3082,7 +3151,7 @@ fn pre_signed_mints_should_work() {
Error::<Test>::AlreadyExists
);
assert_ok!(Nfts::burn(RuntimeOrigin::signed(user_2.clone()), 0, 0, Some(user_2.clone())));
assert_ok!(Nfts::burn(RuntimeOrigin::signed(user_2.clone()), 0, 0));
assert_eq!(Balances::free_balance(&user_2), 100 - 6);
// validate the `only_account` field