[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
+69 -71
View File
@@ -30,6 +30,7 @@
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod migration;
#[cfg(test)]
pub mod mock;
#[cfg(test)]
@@ -58,6 +59,9 @@ pub use pallet::*;
pub use types::*;
pub use weights::WeightInfo;
/// The log target of this pallet.
pub const LOG_TARGET: &'static str = "runtime::nfts";
type AccountIdLookupOf<T> = <<T as SystemConfig>::Lookup as StaticLookup>::Source;
#[frame_support::pallet]
@@ -67,9 +71,13 @@ pub mod pallet {
use frame_system::pallet_prelude::*;
use sp_runtime::traits::{IdentifyAccount, Verify};
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T, I = ()>(_);
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<CollectionId, ItemId> {
@@ -628,6 +636,8 @@ pub mod pallet {
MaxAttributesLimitReached,
/// The provided namespace isn't supported in this call.
WrongNamespace,
/// Can't delete non-empty collections.
CollectionNotEmpty,
}
#[pallet::call]
@@ -719,20 +729,22 @@ pub mod pallet {
/// The origin must conform to `ForceOrigin` or must be `Signed` and the sender must be the
/// owner of the `collection`.
///
/// NOTE: The collection must have 0 items to be destroyed.
///
/// - `collection`: The identifier of the collection to be destroyed.
/// - `witness`: Information on the items minted in the collection. This must be
/// correct.
///
/// Emits `Destroyed` event when successful.
///
/// Weight: `O(n + m)` where:
/// - `n = witness.items`
/// Weight: `O(m + c + a)` where:
/// - `m = witness.item_metadatas`
/// - `c = witness.item_configs`
/// - `a = witness.attributes`
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::destroy(
witness.items,
witness.item_metadatas,
witness.item_metadatas,
witness.item_configs,
witness.attributes,
))]
pub fn destroy(
@@ -746,8 +758,8 @@ pub mod pallet {
let details = Self::do_destroy_collection(collection, witness, maybe_check_owner)?;
Ok(Some(T::WeightInfo::destroy(
details.items,
details.item_metadatas,
details.item_configs,
details.attributes,
))
.into())
@@ -755,7 +767,7 @@ pub mod pallet {
/// Mint an item of a particular collection.
///
/// The origin must be Signed and the sender must be the Issuer of the `collection`.
/// The origin must be Signed and the sender must comply with the `mint_settings` rules.
///
/// - `collection`: The collection of the item to be minted.
/// - `item`: An identifier of the new item.
@@ -789,11 +801,6 @@ pub mod pallet {
mint_to.clone(),
item_config,
|collection_details, collection_config| {
// Issuer can mint regardless of mint settings
if Self::has_role(&collection, &caller, CollectionRole::Issuer) {
return Ok(())
}
let mint_settings = collection_config.mint_settings;
let now = frame_system::Pallet::<T>::block_number();
@@ -805,7 +812,12 @@ pub mod pallet {
}
match mint_settings.mint_type {
MintType::Issuer => return Err(Error::<T, I>::NoPermission.into()),
MintType::Issuer => {
ensure!(
Self::has_role(&collection, &caller, CollectionRole::Issuer),
Error::<T, I>::NoPermission
);
},
MintType::HolderOf(collection_id) => {
let MintWitness { owned_item } =
witness_data.ok_or(Error::<T, I>::BadWitness)?;
@@ -899,38 +911,30 @@ pub mod pallet {
/// Destroy a single item.
///
/// Origin must be Signed and the signing account must be either:
/// - the Admin of the `collection`;
/// - the Owner of the `item`;
/// The origin must conform to `ForceOrigin` or must be Signed and the signing account must
/// be the owner of the `item`.
///
/// - `collection`: The collection of the item to be burned.
/// - `item`: The item to be burned.
/// - `check_owner`: If `Some` then the operation will fail with `WrongOwner` unless the
/// item is owned by this value.
///
/// Emits `Burned` with the actual amount burned.
/// Emits `Burned`.
///
/// Weight: `O(1)`
/// Modes: `check_owner.is_some()`.
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::burn())]
pub fn burn(
origin: OriginFor<T>,
collection: T::CollectionId,
item: T::ItemId,
check_owner: Option<AccountIdLookupOf<T>>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let check_owner = check_owner.map(T::Lookup::lookup).transpose()?;
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_burn(collection, item, |details| {
let is_admin = Self::has_role(&collection, &origin, CollectionRole::Admin);
let is_permitted = is_admin || details.owner == origin;
ensure!(is_permitted, Error::<T, I>::NoPermission);
ensure!(
check_owner.map_or(true, |o| o == details.owner),
Error::<T, I>::WrongOwner
);
if let Some(check_origin) = maybe_check_origin {
ensure!(details.owner == check_origin, Error::<T, I>::NoPermission);
}
Ok(())
})
}
@@ -938,7 +942,6 @@ pub mod pallet {
/// Move an item from the sender account to another.
///
/// Origin must be Signed and the signing account must be either:
/// - the Admin of the `collection`;
/// - the Owner of the `item`;
/// - the approved delegate for the `item` (in this case, the approval is reset).
///
@@ -962,8 +965,7 @@ pub mod pallet {
let dest = T::Lookup::lookup(dest)?;
Self::do_transfer(collection, item, dest, |_, details| {
let is_admin = Self::has_role(&collection, &origin, CollectionRole::Admin);
if details.owner != origin && !is_admin {
if details.owner != origin {
let deadline =
details.approvals.get(&origin).ok_or(Error::<T, I>::NoPermission)?;
if let Some(d) = deadline {
@@ -1086,12 +1088,13 @@ pub mod pallet {
/// Disallows specified settings for the whole collection.
///
/// Origin must be Signed and the sender should be the Freezer of the `collection`.
/// Origin must be Signed and the sender should be the Owner of the `collection`.
///
/// - `collection`: The collection to be locked.
/// - `lock_settings`: The settings to be locked.
///
/// Note: it's possible to only lock(set) the setting, but not to unset it.
///
/// Emits `CollectionLocked`.
///
/// Weight: `O(1)`
@@ -1243,7 +1246,6 @@ pub mod pallet {
///
/// Origin must be either:
/// - the `Force` origin;
/// - `Signed` with the signer being the Admin of the `collection`;
/// - `Signed` with the signer being the Owner of the `item`;
///
/// Arguments:
@@ -1273,7 +1275,6 @@ pub mod pallet {
///
/// Origin must be either:
/// - the `Force` origin;
/// - `Signed` with the signer being the Admin of the `collection`;
/// - `Signed` with the signer being the Owner of the `item`;
///
/// Arguments:
@@ -1298,8 +1299,8 @@ pub mod pallet {
/// Disallows changing the metadata or attributes of the item.
///
/// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the
/// `collection`.
/// Origin must be either `ForceOrigin` or Signed and the sender should be the Admin
/// of the `collection`.
///
/// - `collection`: The collection if the `item`.
/// - `item`: An item to be locked.
@@ -1307,8 +1308,8 @@ pub mod pallet {
/// - `lock_attributes`: Specifies whether the attributes in the `CollectionOwner` namespace
/// should be locked.
///
/// Note: `lock_attributes` affects the attributes in the `CollectionOwner` namespace
/// only. When the metadata or attributes are locked, it won't be possible the unlock them.
/// Note: `lock_attributes` affects the attributes in the `CollectionOwner` namespace only.
/// When the metadata or attributes are locked, it won't be possible the unlock them.
///
/// Emits `ItemPropertiesLocked`.
///
@@ -1322,11 +1323,11 @@ pub mod pallet {
lock_metadata: bool,
lock_attributes: bool,
) -> DispatchResult {
let maybe_check_owner = T::ForceOrigin::try_origin(origin)
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_lock_item_properties(
maybe_check_owner,
maybe_check_origin,
collection,
item,
lock_metadata,
@@ -1337,7 +1338,7 @@ pub mod pallet {
/// Set an attribute for a collection or item.
///
/// Origin must be Signed and must conform to the namespace ruleset:
/// - `CollectionOwner` namespace could be modified by the `collection` owner only;
/// - `CollectionOwner` namespace could be modified by the `collection` Admin only;
/// - `ItemOwner` namespace could be modified by the `maybe_item` owner only. `maybe_item`
/// should be set in that case;
/// - `Account(AccountId)` namespace could be modified only when the `origin` was given a
@@ -1367,15 +1368,12 @@ pub mod pallet {
value: BoundedVec<u8, T::ValueLimit>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
Self::do_set_attribute(
origin.clone(),
collection,
maybe_item,
namespace,
key,
value,
origin,
)
let depositor = match namespace {
AttributeNamespace::CollectionOwner =>
Self::collection_owner(collection).ok_or(Error::<T, I>::UnknownCollection)?,
_ => origin.clone(),
};
Self::do_set_attribute(origin, collection, maybe_item, namespace, key, value, depositor)
}
/// Force-set an attribute for a collection or item.
@@ -1490,7 +1488,7 @@ pub mod pallet {
/// Set the metadata for an item.
///
/// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the
/// Origin must be either `ForceOrigin` or Signed and the sender should be the Admin of the
/// `collection`.
///
/// If the origin is Signed, then funds of signer are reserved according to the formula:
@@ -1512,15 +1510,15 @@ pub mod pallet {
item: T::ItemId,
data: BoundedVec<u8, T::StringLimit>,
) -> DispatchResult {
let maybe_check_owner = T::ForceOrigin::try_origin(origin)
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_set_item_metadata(maybe_check_owner, collection, item, data, None)
Self::do_set_item_metadata(maybe_check_origin, collection, item, data, None)
}
/// Clear the metadata for an item.
///
/// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the
/// Origin must be either `ForceOrigin` or Signed and the sender should be the Admin of the
/// `collection`.
///
/// Any deposit is freed for the collection's owner.
@@ -1538,15 +1536,15 @@ pub mod pallet {
collection: T::CollectionId,
item: T::ItemId,
) -> DispatchResult {
let maybe_check_owner = T::ForceOrigin::try_origin(origin)
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_clear_item_metadata(maybe_check_owner, collection, item)
Self::do_clear_item_metadata(maybe_check_origin, collection, item)
}
/// Set the metadata for a collection.
///
/// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of
/// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Admin of
/// the `collection`.
///
/// If the origin is `Signed`, then funds of signer are reserved according to the formula:
@@ -1566,15 +1564,15 @@ pub mod pallet {
collection: T::CollectionId,
data: BoundedVec<u8, T::StringLimit>,
) -> DispatchResult {
let maybe_check_owner = T::ForceOrigin::try_origin(origin)
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_set_collection_metadata(maybe_check_owner, collection, data)
Self::do_set_collection_metadata(maybe_check_origin, collection, data)
}
/// Clear the metadata for a collection.
///
/// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of
/// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Admin of
/// the `collection`.
///
/// Any deposit is freed for the collection's owner.
@@ -1590,10 +1588,10 @@ pub mod pallet {
origin: OriginFor<T>,
collection: T::CollectionId,
) -> DispatchResult {
let maybe_check_owner = T::ForceOrigin::try_origin(origin)
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_clear_collection_metadata(maybe_check_owner, collection)
Self::do_clear_collection_metadata(maybe_check_origin, collection)
}
/// Set (or reset) the acceptance of ownership for a particular account.
@@ -1640,8 +1638,8 @@ pub mod pallet {
/// Update mint settings.
///
/// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of
/// the `collection`.
/// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Issuer
/// of the `collection`.
///
/// - `collection`: The identifier of the collection to change.
/// - `mint_settings`: The new mint settings.
@@ -1658,15 +1656,15 @@ pub mod pallet {
T::CollectionId,
>,
) -> DispatchResult {
let maybe_check_owner = T::ForceOrigin::try_origin(origin)
let maybe_check_origin = T::ForceOrigin::try_origin(origin)
.map(|_| None)
.or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?;
Self::do_update_mint_settings(maybe_check_owner, collection, mint_settings)
Self::do_update_mint_settings(maybe_check_origin, collection, mint_settings)
}
/// Set (or reset) the price for an item.
///
/// Origin must be Signed and must be the owner of the asset `item`.
/// Origin must be Signed and must be the owner of the `item`.
///
/// - `collection`: The collection of the item.
/// - `item`: The item to set the price for.
@@ -1827,7 +1825,7 @@ pub mod pallet {
/// its metadata, attributes, who can mint it (`None` for anyone) and until what block
/// number.
/// - `signature`: The signature of the `data` object.
/// - `signer`: The `data` object's signer. Should be an owner of the collection.
/// - `signer`: The `data` object's signer. Should be an Issuer of the collection.
///
/// Emits `Issued` on success.
/// Emits `AttributeSet` if the attributes were provided.
@@ -1853,7 +1851,7 @@ pub mod pallet {
/// - `data`: The pre-signed approval that consists of the information about the item,
/// attributes to update and until what block number.
/// - `signature`: The signature of the `data` object.
/// - `signer`: The `data` object's signer. Should be an owner of the collection for the
/// - `signer`: The `data` object's signer. Should be an Admin of the collection for the
/// `CollectionOwner` namespace.
///
/// Emits `AttributeSet` for each provided attribute.