diff --git a/Cargo.lock b/Cargo.lock index fca6465198aa6..c8a7299835a06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4378,6 +4378,7 @@ dependencies = [ "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", + "pallet-uniques", "pallet-utility", "pallet-vesting", "parity-scale-codec", @@ -5619,6 +5620,21 @@ dependencies = [ "sp-storage", ] +[[package]] +name = "pallet-uniques" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-utility" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5bd83b70f4c2b..8b613c021a9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ members = [ "frame/transaction-payment/rpc/runtime-api", "frame/treasury", "frame/tips", + "frame/uniques", "frame/utility", "frame/vesting", "primitives/allocator", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 335d9a1aa2a98..ca1ed7f3dcc09 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -85,6 +85,7 @@ pallet-treasury = { version = "3.0.0", default-features = false, path = "../../. pallet-utility = { version = "3.0.0", default-features = false, path = "../../../frame/utility" } pallet-transaction-payment = { version = "3.0.0", default-features = false, path = "../../../frame/transaction-payment" } pallet-transaction-payment-rpc-runtime-api = { version = "3.0.0", default-features = false, path = "../../../frame/transaction-payment/rpc/runtime-api/" } +pallet-uniques = { version = "3.0.0", default-features = false, path = "../../../frame/uniques" } pallet-vesting = { version = "3.0.0", default-features = false, path = "../../../frame/vesting" } max-encoded-len = { version = "3.0.0", default-features = false, path = "../../../max-encoded-len", features = [ "derive" ] } @@ -157,6 +158,7 @@ std = [ "sp-version/std", "pallet-society/std", "pallet-recovery/std", + "pallet-uniques/std", "pallet-vesting/std", "log/std", "frame-try-runtime/std", @@ -194,6 +196,7 @@ runtime-benchmarks = [ "pallet-tips/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", "pallet-utility/runtime-benchmarks", + "pallet-uniques/runtime-benchmarks", "pallet-vesting/runtime-benchmarks", "pallet-offences-benchmarking", "pallet-session-benchmarking", @@ -237,6 +240,7 @@ try-runtime = [ "pallet-utility/try-runtime", "pallet-society/try-runtime", "pallet-recovery/try-runtime", + "pallet-uniques/try-runtime", "pallet-vesting/try-runtime", "pallet-gilt/try-runtime", ] diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index c51799d11a943..f92ca963bb62d 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -114,7 +114,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to 0. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 266, + spec_version: 267, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 2, @@ -1090,6 +1090,30 @@ impl pallet_gilt::Config for Runtime { type WeightInfo = pallet_gilt::weights::SubstrateWeight; } +parameter_types! { + pub const ClassDeposit: Balance = 100 * DOLLARS; + pub const InstanceDeposit: Balance = 1 * DOLLARS; + pub const KeyLimit: u32 = 32; + pub const ValueLimit: u32 = 256; +} + +impl pallet_uniques::Config for Runtime { + type Event = Event; + type ClassId = u32; + type InstanceId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type ClassDeposit = ClassDeposit; + type InstanceDeposit = InstanceDeposit; + type MetadataDepositBase = MetadataDepositBase; + type AttributeDepositBase = MetadataDepositBase; + type DepositPerByte = MetadataDepositPerByte; + type StringLimit = StringLimit; + type KeyLimit = KeyLimit; + type ValueLimit = ValueLimit; + type WeightInfo = pallet_uniques::weights::SubstrateWeight; +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -1134,6 +1158,7 @@ construct_runtime!( Mmr: pallet_mmr::{Pallet, Storage}, Lottery: pallet_lottery::{Pallet, Call, Storage, Event}, Gilt: pallet_gilt::{Pallet, Call, Storage, Event, Config}, + Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, } ); @@ -1508,6 +1533,7 @@ impl_runtime_apis! { add_benchmark!(params, batches, pallet_timestamp, Timestamp); add_benchmark!(params, batches, pallet_tips, Tips); add_benchmark!(params, batches, pallet_treasury, Treasury); + add_benchmark!(params, batches, pallet_uniques, Uniques); add_benchmark!(params, batches, pallet_utility, Utility); add_benchmark!(params, batches, pallet_vesting, Vesting); diff --git a/frame/assets/src/impl_fungibles.rs b/frame/assets/src/impl_fungibles.rs index d0ab13072a88d..71951bae11165 100644 --- a/frame/assets/src/impl_fungibles.rs +++ b/frame/assets/src/impl_fungibles.rs @@ -127,26 +127,26 @@ impl, I: 'static> fungibles::Unbalanced for Pallet Result + -> Result { let f = DebitFlags { keep_alive: false, best_effort: false }; Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) } fn decrease_balance_at_most(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance) - -> Self::Balance + -> Self::Balance { let f = DebitFlags { keep_alive: false, best_effort: true }; Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) .unwrap_or(Zero::zero()) } fn increase_balance(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance) - -> Result + -> Result { Self::increase_balance(asset, who, amount, |_| Ok(()))?; Ok(amount) } fn increase_balance_at_most(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance) - -> Self::Balance + -> Self::Balance { match Self::increase_balance(asset, who, amount, |_| Ok(())) { Ok(()) => amount, diff --git a/frame/assets/src/lib.rs b/frame/assets/src/lib.rs index e81fca20db81b..333dbad836462 100644 --- a/frame/assets/src/lib.rs +++ b/frame/assets/src/lib.rs @@ -417,8 +417,6 @@ pub mod pallet { /// - `owner`: The owner of this class of assets. The owner has full superuser permissions /// over this asset, but may later change and configure the permissions using `transfer_ownership` /// and `set_team`. - /// - `max_zombies`: The total number of accounts which may hold assets in this class yet - /// have no existential deposit. /// - `min_balance`: The minimum balance of this new asset that any single account must /// have. If an account's balance is reduced below this, then it collapses to zero. /// @@ -588,8 +586,8 @@ pub mod pallet { /// to zero. /// /// Weight: `O(1)` - /// Modes: Pre-existence of `target`; Post-existence of sender; Prior & post zombie-status - /// of sender; Account pre-existence of `target`. + /// Modes: Pre-existence of `target`; Post-existence of sender; Account pre-existence of + /// `target`. #[pallet::weight(T::WeightInfo::transfer())] pub(super) fn transfer( origin: OriginFor, @@ -624,8 +622,8 @@ pub mod pallet { /// to zero. /// /// Weight: `O(1)` - /// Modes: Pre-existence of `target`; Post-existence of sender; Prior & post zombie-status - /// of sender; Account pre-existence of `target`. + /// Modes: Pre-existence of `target`; Post-existence of sender; Account pre-existence of + /// `target`. #[pallet::weight(T::WeightInfo::transfer_keep_alive())] pub(super) fn transfer_keep_alive( origin: OriginFor, @@ -661,8 +659,8 @@ pub mod pallet { /// to zero. /// /// Weight: `O(1)` - /// Modes: Pre-existence of `dest`; Post-existence of `source`; Prior & post zombie-status - /// of `source`; Account pre-existence of `dest`. + /// Modes: Pre-existence of `dest`; Post-existence of `source`; Account pre-existence of + /// `dest`. #[pallet::weight(T::WeightInfo::force_transfer())] pub(super) fn force_transfer( origin: OriginFor, @@ -779,7 +777,7 @@ pub mod pallet { /// /// Origin must be Signed and the sender should be the Admin of the asset `id`. /// - /// - `id`: The identifier of the asset to be frozen. + /// - `id`: The identifier of the asset to be thawed. /// /// Emits `Thawed`. /// diff --git a/frame/support/src/storage/bounded_vec.rs b/frame/support/src/storage/bounded_vec.rs index a4e8c50918a0d..fe58b5cd476a9 100644 --- a/frame/support/src/storage/bounded_vec.rs +++ b/frame/support/src/storage/bounded_vec.rs @@ -94,6 +94,12 @@ impl BoundedVec { } } +impl> From> for Vec { + fn from(x: BoundedVec) -> Vec { + x.0 + } +} + impl> BoundedVec { /// Get the bound of the type in `usize`. pub fn bound() -> usize { diff --git a/frame/support/src/storage/types/nmap.rs b/frame/support/src/storage/types/nmap.rs index f018ccc38b4fe..e1f5feb956ef3 100755 --- a/frame/support/src/storage/types/nmap.rs +++ b/frame/support/src/storage/types/nmap.rs @@ -272,7 +272,7 @@ where /// Iter over all value of the storage. /// - /// NOTE: If a value failed to decode becaues storage is corrupted then it is skipped. + /// NOTE: If a value failed to decode because storage is corrupted then it is skipped. pub fn iter_values() -> crate::storage::PrefixIterator { >::iter_values() } diff --git a/frame/uniques/Cargo.toml b/frame/uniques/Cargo.toml new file mode 100644 index 0000000000000..f007744dc64a2 --- /dev/null +++ b/frame/uniques/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-uniques" +version = "3.0.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME NFT asset management pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +sp-std = { version = "3.0.0", default-features = false, path = "../../primitives/std" } +sp-core = { version = "3.0.0", default-features = false, path = "../../primitives/core" } +sp-runtime = { version = "3.0.0", default-features = false, path = "../../primitives/runtime" } +frame-support = { version = "3.0.0", default-features = false, path = "../support" } +frame-system = { version = "3.0.0", default-features = false, path = "../system" } +frame-benchmarking = { version = "3.1.0", default-features = false, path = "../benchmarking", optional = true } + +[dev-dependencies] +sp-std = { version = "3.0.0", path = "../../primitives/std" } +sp-core = { version = "3.0.0", path = "../../primitives/core" } +sp-io = { version = "3.0.0", path = "../../primitives/io" } +pallet-balances = { version = "3.0.0", path = "../balances" } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "sp-runtime/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/uniques/README.md b/frame/uniques/README.md new file mode 100644 index 0000000000000..b924e338452ff --- /dev/null +++ b/frame/uniques/README.md @@ -0,0 +1,78 @@ +# Uniques Module + +A simple, secure module for dealing with non-fungible assets. + +## Overview + +The Uniques module provides functionality for asset management of non-fungible asset classes, including: + +* Asset Issuance +* Asset Transfer +* Asset Destruction + +To use it in your runtime, you need to implement the assets [`uniques::Config`](https://docs.rs/pallet-uniques/latest/pallet_uniques/pallet/trait.Config.html). + +The supported dispatchable functions are documented in the [`uniques::Call`](https://docs.rs/pallet-uniques/latest/pallet_uniques/pallet/enum.Call.html) enum. + +### Terminology + +* **Asset issuance:** The creation of a new asset instance. +* **Asset transfer:** The action of transferring an asset instance from one account to another. +* **Asset burning:** The destruction of an asset instance. +* **Non-fungible asset:** An asset for which each unit has unique characteristics. There is exactly + one instance of such an asset in existance and there is exactly one owning account. + +### Goals + +The Uniques pallet in Substrate is designed to make the following possible: + +* Allow accounts to permissionlessly create asset classes (collections of asset instances). +* Allow a named (permissioned) account to mint and burn unique assets within a class. +* Move asset instances between accounts permissionlessly. +* Allow a named (permissioned) account to freeze and unfreeze unique assets within a + class or the entire class. +* Allow the owner of an asset instance to delegate the ability to transfer the asset to some + named third-party. + +## Interface + +### Permissionless dispatchables +* `create`: Create a new asset class by placing a deposit. +* `transfer`: Transfer an asset instance to a new owner. +* `redeposit`: Update the deposit amount of an asset instance, potentially freeing funds. +* `approve_transfer`: Name a delegate who may authorise a transfer. +* `cancel_approval`: Revert the effects of a previous `approve_transfer`. + +### Permissioned dispatchables +* `destroy`: Destroy an asset class. +* `mint`: Mint a new asset instance within an asset class. +* `burn`: Burn an asset instance within an asset class. +* `freeze`: Prevent an individual asset from being transferred. +* `thaw`: Revert the effects of a previous `freeze`. +* `freeze_class`: Prevent all asset within a class from being transferred. +* `thaw_class`: Revert the effects of a previous `freeze_class`. +* `transfer_ownership`: Alter the owner of an asset class, moving all associated deposits. +* `set_team`: Alter the permissioned accounts of an asset class. + +### Metadata (permissioned) dispatchables +* `set_attribute`: Set a metadata attribute of an asset instance or class. +* `clear_attribute`: Remove a metadata attribute of an asset instance or class. +* `set_metadata`: Set general metadata of an asset instance. +* `clear_metadata`: Remove general metadata of an asset instance. +* `set_class_metadata`: Set general metadata of an asset class. +* `clear_class_metadata`: Remove general metadata of an asset class. + +### Force (i.e. governance) dispatchables +* `force_create`: Create a new asset class. +* `force_asset_status`: Alter the underlying characteristics of an asset class. + +Please refer to the [`Call`](https://docs.rs/pallet-assets/latest/pallet_assets/enum.Call.html) enum +and its associated variants for documentation on each function. + +## Related Modules + +* [`System`](https://docs.rs/frame-system/latest/frame_system/) +* [`Support`](https://docs.rs/frame-support/latest/frame_support/) +* [`Assets`](https://docs.rs/pallet-assets/latest/pallet_assetss/) + +License: Apache-2.0 diff --git a/frame/uniques/src/benchmarking.rs b/frame/uniques/src/benchmarking.rs new file mode 100644 index 0000000000000..ca6d656bd5005 --- /dev/null +++ b/frame/uniques/src/benchmarking.rs @@ -0,0 +1,376 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Assets pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use sp_std::{prelude::*, convert::TryInto}; +use super::*; +use sp_runtime::traits::Bounded; +use frame_system::RawOrigin as SystemOrigin; +use frame_benchmarking::{ + benchmarks_instance_pallet, account, whitelisted_caller, whitelist_account, impl_benchmark_test_suite +}; +use frame_support::{traits::{Get, EnsureOrigin}, dispatch::UnfilteredDispatchable, BoundedVec}; + +use crate::Pallet as Uniques; + +const SEED: u32 = 0; + +fn create_class, I: 'static>() + -> (T::ClassId, T::AccountId, ::Source) +{ + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let class = Default::default(); + T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); + assert!(Uniques::::create( + SystemOrigin::Signed(caller.clone()).into(), + class, + caller_lookup.clone(), + ).is_ok()); + (class, caller, caller_lookup) +} + +fn add_class_metadata, I: 'static>() + -> (T::AccountId, ::Source) +{ + let caller = Class::::get(T::ClassId::default()).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + assert!(Uniques::::set_class_metadata( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + vec![0; T::StringLimit::get() as usize].try_into().unwrap(), + false, + ).is_ok()); + (caller, caller_lookup) +} + +fn mint_instance, I: 'static>(index: u16) + -> (T::InstanceId, T::AccountId, ::Source) +{ + let caller = Class::::get(T::ClassId::default()).unwrap().admin; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let instance = index.into(); + assert!(Uniques::::mint( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + instance, + caller_lookup.clone(), + ).is_ok()); + (instance, caller, caller_lookup) +} + +fn add_instance_metadata, I: 'static>(instance: T::InstanceId) + -> (T::AccountId, ::Source) +{ + let caller = Class::::get(T::ClassId::default()).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + assert!(Uniques::::set_metadata( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + instance, + vec![0; T::StringLimit::get() as usize].try_into().unwrap(), + false, + ).is_ok()); + (caller, caller_lookup) +} + +fn add_instance_attribute, I: 'static>(instance: T::InstanceId) + -> (BoundedVec, T::AccountId, ::Source) +{ + let caller = Class::::get(T::ClassId::default()).unwrap().owner; + if caller != whitelisted_caller() { + whitelist_account!(caller); + } + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let key: BoundedVec<_, _> = vec![0; T::KeyLimit::get() as usize].try_into().unwrap(); + assert!(Uniques::::set_attribute( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + Some(instance), + key.clone(), + vec![0; T::ValueLimit::get() as usize].try_into().unwrap(), + ).is_ok()); + (key, caller, caller_lookup) +} + +fn assert_last_event, I: 'static>(generic_event: >::Event) { + let events = frame_system::Pallet::::events(); + let system_event: ::Event = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +benchmarks_instance_pallet! { + create { + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + T::Currency::make_free_balance_be(&caller, DepositBalanceOf::::max_value()); + }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup) + verify { + assert_last_event::(Event::Created(Default::default(), caller.clone(), caller).into()); + } + + force_create { + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + }: _(SystemOrigin::Root, Default::default(), caller_lookup, true) + verify { + assert_last_event::(Event::ForceCreated(Default::default(), caller).into()); + } + + destroy { + let n in 0 .. 1_000; + let m in 0 .. 1_000; + let a in 0 .. 1_000; + + let (class, caller, caller_lookup) = create_class::(); + add_class_metadata::(); + for i in 0..n { + mint_instance::(i as u16); + } + for i in 0..m { + add_instance_metadata::((i as u16).into()); + } + for i in 0..a { + add_instance_attribute::((i as u16).into()); + } + let witness = Class::::get(class).unwrap().destroy_witness(); + }: _(SystemOrigin::Signed(caller), class, witness) + verify { + assert_last_event::(Event::Destroyed(class).into()); + } + + mint { + let (class, caller, caller_lookup) = create_class::(); + let instance = Default::default(); + }: _(SystemOrigin::Signed(caller.clone()), class, instance, caller_lookup) + verify { + assert_last_event::(Event::Issued(class, instance, caller).into()); + } + + burn { + let (class, caller, caller_lookup) = create_class::(); + let (instance, ..) = mint_instance::(0); + }: _(SystemOrigin::Signed(caller.clone()), class, instance, Some(caller_lookup)) + verify { + assert_last_event::(Event::Burned(class, instance, caller).into()); + } + + transfer { + let (class, caller, caller_lookup) = create_class::(); + let (instance, ..) = mint_instance::(Default::default()); + + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + }: _(SystemOrigin::Signed(caller.clone()), class, instance, target_lookup) + verify { + assert_last_event::(Event::Transferred(class, instance, caller, target).into()); + } + + redeposit { + let i in 0 .. 5_000; + let (class, caller, caller_lookup) = create_class::(); + let instances = (0..i).map(|x| mint_instance::(x as u16).0).collect::>(); + Uniques::::force_asset_status( + SystemOrigin::Root.into(), + class, + caller_lookup.clone(), + caller_lookup.clone(), + caller_lookup.clone(), + caller_lookup.clone(), + true, + false, + )?; + }: _(SystemOrigin::Signed(caller.clone()), class, instances.clone()) + verify { + assert_last_event::(Event::Redeposited(class, instances).into()); + } + + freeze { + let (class, caller, caller_lookup) = create_class::(); + let (instance, ..) = mint_instance::(Default::default()); + }: _(SystemOrigin::Signed(caller.clone()), Default::default(), Default::default()) + verify { + assert_last_event::(Event::Frozen(Default::default(), Default::default()).into()); + } + + thaw { + let (class, caller, caller_lookup) = create_class::(); + let (instance, ..) = mint_instance::(Default::default()); + Uniques::::freeze( + SystemOrigin::Signed(caller.clone()).into(), + class, + instance, + )?; + }: _(SystemOrigin::Signed(caller.clone()), class, instance) + verify { + assert_last_event::(Event::Thawed(class, instance).into()); + } + + freeze_class { + let (class, caller, caller_lookup) = create_class::(); + }: _(SystemOrigin::Signed(caller.clone()), class) + verify { + assert_last_event::(Event::ClassFrozen(class).into()); + } + + thaw_class { + let (class, caller, caller_lookup) = create_class::(); + let origin = SystemOrigin::Signed(caller.clone()).into(); + Uniques::::freeze_class(origin, class)?; + }: _(SystemOrigin::Signed(caller.clone()), class) + verify { + assert_last_event::(Event::ClassThawed(class).into()); + } + + transfer_ownership { + let (class, caller, _) = create_class::(); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); + }: _(SystemOrigin::Signed(caller), class, target_lookup) + verify { + assert_last_event::(Event::OwnerChanged(class, target).into()); + } + + set_team { + let (class, caller, _) = create_class::(); + let target0 = T::Lookup::unlookup(account("target", 0, SEED)); + let target1 = T::Lookup::unlookup(account("target", 1, SEED)); + let target2 = T::Lookup::unlookup(account("target", 2, SEED)); + }: _(SystemOrigin::Signed(caller), Default::default(), target0.clone(), target1.clone(), target2.clone()) + verify { + assert_last_event::(Event::TeamChanged( + class, + account("target", 0, SEED), + account("target", 1, SEED), + account("target", 2, SEED), + ).into()); + } + + force_asset_status { + let (class, caller, caller_lookup) = create_class::(); + let origin = T::ForceOrigin::successful_origin(); + let call = Call::::force_asset_status( + class, + caller_lookup.clone(), + caller_lookup.clone(), + caller_lookup.clone(), + caller_lookup.clone(), + true, + false, + ); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::AssetStatusChanged(class).into()); + } + + set_attribute { + let key: BoundedVec<_, _> = vec![0u8; T::KeyLimit::get() as usize].try_into().unwrap(); + let value: BoundedVec<_, _> = vec![0u8; T::ValueLimit::get() as usize].try_into().unwrap(); + + let (class, caller, _) = create_class::(); + let (instance, ..) = mint_instance::(0); + add_instance_metadata::(instance); + }: _(SystemOrigin::Signed(caller), class, Some(instance), key.clone(), value.clone()) + verify { + assert_last_event::(Event::AttributeSet(class, Some(instance), key, value).into()); + } + + clear_attribute { + let (class, caller, _) = create_class::(); + let (instance, ..) = mint_instance::(0); + add_instance_metadata::(instance); + let (key, ..) = add_instance_attribute::(instance); + }: _(SystemOrigin::Signed(caller), class, Some(instance), key.clone()) + verify { + assert_last_event::(Event::AttributeCleared(class, Some(instance), key).into()); + } + + set_metadata { + let data: BoundedVec<_, _> = vec![0u8; T::StringLimit::get() as usize].try_into().unwrap(); + + let (class, caller, _) = create_class::(); + let (instance, ..) = mint_instance::(0); + }: _(SystemOrigin::Signed(caller), class, instance, data.clone(), false) + verify { + assert_last_event::(Event::MetadataSet(class, instance, data, false).into()); + } + + clear_metadata { + let (class, caller, _) = create_class::(); + let (instance, ..) = mint_instance::(0); + add_instance_metadata::(instance); + }: _(SystemOrigin::Signed(caller), class, instance) + verify { + assert_last_event::(Event::MetadataCleared(class, instance).into()); + } + + set_class_metadata { + let data: BoundedVec<_, _> = vec![0u8; T::StringLimit::get() as usize].try_into().unwrap(); + + let (class, caller, _) = create_class::(); + }: _(SystemOrigin::Signed(caller), class, data.clone(), false) + verify { + assert_last_event::(Event::ClassMetadataSet(class, data, false).into()); + } + + clear_class_metadata { + let (class, caller, _) = create_class::(); + add_class_metadata::(); + }: _(SystemOrigin::Signed(caller), class) + verify { + assert_last_event::(Event::ClassMetadataCleared(class).into()); + } + + approve_transfer { + let (class, caller, _) = create_class::(); + let (instance, ..) = mint_instance::(0); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + }: _(SystemOrigin::Signed(caller.clone()), class, instance, delegate_lookup) + verify { + assert_last_event::(Event::ApprovedTransfer(class, instance, caller, delegate).into()); + } + + cancel_approval { + let (class, caller, _) = create_class::(); + let (instance, ..) = mint_instance::(0); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let origin = SystemOrigin::Signed(caller.clone()).into(); + Uniques::::approve_transfer(origin, class, instance, delegate_lookup.clone())?; + }: _(SystemOrigin::Signed(caller.clone()), class, instance, Some(delegate_lookup)) + verify { + assert_last_event::(Event::ApprovalCancelled(class, instance, caller, delegate).into()); + } +} + +impl_benchmark_test_suite!(Uniques, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/frame/uniques/src/lib.rs b/frame/uniques/src/lib.rs new file mode 100644 index 0000000000000..21142a3a92ce0 --- /dev/null +++ b/frame/uniques/src/lib.rs @@ -0,0 +1,1289 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Unique (Assets) Module +//! +//! A simple, secure module for dealing with non-fungible assets. +//! +//! ## Related Modules +//! +//! * [`System`](../frame_system/index.html) +//! * [`Support`](../frame_support/index.html) + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod weights; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +pub mod mock; +#[cfg(test)] +mod tests; + +mod types; +pub use types::*; + +use sp_std::prelude::*; +use sp_runtime::{RuntimeDebug, ArithmeticError, traits::{Zero, StaticLookup, Saturating}}; +use codec::{Encode, Decode, HasCompact}; +use frame_support::traits::{Currency, ReservableCurrency, BalanceStatus::Reserved}; +use frame_system::Config as SystemConfig; + +pub use weights::WeightInfo; +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + /// The module configuration trait. + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// Identifier for the class of asset. + type ClassId: Member + Parameter + Default + Copy + HasCompact; + + /// The type used to identify a unique asset within an asset class. + type InstanceId: Member + Parameter + Default + Copy + HasCompact + From; + + /// The currency mechanism, used for paying for reserves. + type Currency: ReservableCurrency; + + /// The origin which may forcibly create or destroy an asset or otherwise alter privileged + /// attributes. + type ForceOrigin: EnsureOrigin; + + /// The basic amount of funds that must be reserved for an asset class. + type ClassDeposit: Get>; + + /// The basic amount of funds that must be reserved for an asset instance. + type InstanceDeposit: Get>; + + /// The basic amount of funds that must be reserved when adding metadata to your asset. + type MetadataDepositBase: Get>; + + /// The basic amount of funds that must be reserved when adding an attribute to an asset. + type AttributeDepositBase: Get>; + + /// The additional funds that must be reserved for the number of bytes store in metadata, + /// either "normal" metadata or attribute metadata. + type DepositPerByte: Get>; + + /// The maximum length of data stored on-chain. + type StringLimit: Get; + + /// The maximum length of an attribute key. + type KeyLimit: Get; + + /// The maximum length of an attribute value. + type ValueLimit: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::storage] + /// Details of an asset class. + pub(super) type Class, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::ClassId, + ClassDetails>, + >; + + #[pallet::storage] + /// The assets held by any given account; set out this way so that assets owned by a single + /// account can be enumerated. + pub(super) type Account, I: 'static = ()> = StorageNMap< + _, + ( + NMapKey, // owner + NMapKey, + NMapKey, + ), + (), + OptionQuery, + >; + + #[pallet::storage] + /// The assets in existence and their ownership details. + pub(super) type Asset, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::ClassId, + Blake2_128Concat, + T::InstanceId, + InstanceDetails>, + OptionQuery, + >; + + #[pallet::storage] + /// Metadata of an asset class. + pub(super) type ClassMetadataOf, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::ClassId, + ClassMetadata, T::StringLimit>, + OptionQuery, + >; + + #[pallet::storage] + /// Metadata of an asset instance. + pub(super) type InstanceMetadataOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::ClassId, + Blake2_128Concat, + T::InstanceId, + InstanceMetadata, T::StringLimit>, + OptionQuery, + >; + + #[pallet::storage] + /// Metadata of an asset class. + pub(super) type Attribute, I: 'static = ()> = StorageNMap< + _, + ( + NMapKey, + NMapKey>, + NMapKey>, + ), + (BoundedVec, DepositBalanceOf), + OptionQuery + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + #[pallet::metadata( + T::AccountId = "AccountId", + T::ClassId = "ClassId", + T::InstanceId = "InstanceId", + )] + pub enum Event, I: 'static = ()> { + /// An asset class was created. \[ class, creator, owner \] + Created(T::ClassId, T::AccountId, T::AccountId), + /// An asset class was force-created. \[ class, owner \] + ForceCreated(T::ClassId, T::AccountId), + /// An asset `class` was destroyed. \[ class \] + Destroyed(T::ClassId), + /// An asset `instace` was issued. \[ class, instance, owner \] + Issued(T::ClassId, T::InstanceId, T::AccountId), + /// An asset `instace` was transferred. \[ class, instance, from, to \] + Transferred(T::ClassId, T::InstanceId, T::AccountId, T::AccountId), + /// An asset `instance` was destroyed. \[ class, instance, owner \] + Burned(T::ClassId, T::InstanceId, T::AccountId), + /// Some asset `instance` was frozen. \[ class, instance \] + Frozen(T::ClassId, T::InstanceId), + /// Some asset `instance` was thawed. \[ class, instance \] + Thawed(T::ClassId, T::InstanceId), + /// Some asset `class` was frozen. \[ class \] + ClassFrozen(T::ClassId), + /// Some asset `class` was thawed. \[ class \] + ClassThawed(T::ClassId), + /// The owner changed \[ class, new_owner \] + OwnerChanged(T::ClassId, T::AccountId), + /// The management team changed \[ class, issuer, admin, freezer \] + TeamChanged(T::ClassId, T::AccountId, T::AccountId, T::AccountId), + /// An `instance` of an asset `class` has been approved by the `owner` for transfer by a + /// `delegate`. + /// \[ class, instance, owner, delegate \] + ApprovedTransfer(T::ClassId, T::InstanceId, T::AccountId, T::AccountId), + /// An approval for a `delegate` account to transfer the `instance` of an asset `class` was + /// cancelled by its `owner`. + /// \[ class, instance, owner, delegate \] + ApprovalCancelled(T::ClassId, T::InstanceId, T::AccountId, T::AccountId), + /// An asset `class` has had its attributes changed by the `Force` origin. + /// \[ class \] + AssetStatusChanged(T::ClassId), + /// New metadata has been set for an asset class. \[ class, data, is_frozen \] + ClassMetadataSet(T::ClassId, BoundedVec, bool), + /// Metadata has been cleared for an asset class. \[ class \] + ClassMetadataCleared(T::ClassId), + /// New metadata has been set for an asset instance. + /// \[ class, instance, data, is_frozen \] + MetadataSet(T::ClassId, T::InstanceId, BoundedVec, bool), + /// Metadata has been cleared for an asset instance. \[ class, instance \] + MetadataCleared(T::ClassId, T::InstanceId), + /// Metadata has been cleared for an asset instance. \[ class, successful_instances \] + Redeposited(T::ClassId, Vec), + /// New attribute metadata has been set for an asset class or instance. + /// \[ class, maybe_instance, key, value \] + AttributeSet( + T::ClassId, + Option, + BoundedVec, + BoundedVec, + ), + /// Attribute metadata has been cleared for an asset class or instance. + /// \[ class, maybe_instance, key, maybe_value \] + AttributeCleared(T::ClassId, Option, BoundedVec), + } + + #[pallet::error] + pub enum Error { + /// The signing account has no permission to do the operation. + NoPermission, + /// The given asset ID is unknown. + Unknown, + /// The asset instance ID has already been used for an asset. + AlreadyExists, + /// The owner turned out to be different to what was expected. + WrongOwner, + /// Invalid witness data given. + BadWitness, + /// The asset ID is already taken. + InUse, + /// The asset instance or class is frozen. + Frozen, + /// The delegate turned out to be different to what was expected. + WrongDelegate, + /// There is no delegate approved. + NoDelegate, + /// No approval exists that would allow the transfer. + Unapproved, + } + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet {} + + impl, I: 'static> Pallet { + /// Get the owner of the asset instance, if the asset exists. + pub fn owner(class: T::ClassId, instance: T::InstanceId) -> Option { + Asset::::get(class, instance).map(|i| i.owner) + } + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Issue a new class of non-fungible assets from a public origin. + /// + /// This new asset class has no assets initially and its owner is the origin. + /// + /// The origin must be Signed and the sender must have sufficient funds free. + /// + /// `AssetDeposit` funds of sender are reserved. + /// + /// Parameters: + /// - `class`: The identifier of the new asset class. This must not be currently in use. + /// - `admin`: The admin of this class of assets. The admin is the initial address of each + /// member of the asset class's admin team. + /// + /// Emits `Created` event when successful. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::create())] + pub(super) fn create( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + admin: ::Source, + ) -> DispatchResult { + let owner = ensure_signed(origin)?; + let admin = T::Lookup::lookup(admin)?; + + ensure!(!Class::::contains_key(class), Error::::InUse); + + let deposit = T::ClassDeposit::get(); + T::Currency::reserve(&owner, deposit)?; + + Class::::insert( + class, + ClassDetails { + owner: owner.clone(), + issuer: admin.clone(), + admin: admin.clone(), + freezer: admin.clone(), + total_deposit: deposit, + free_holding: false, + instances: 0, + instance_metadatas: 0, + attributes: 0, + is_frozen: false, + }, + ); + Self::deposit_event(Event::Created(class, owner, admin)); + Ok(()) + } + + /// Issue a new class of non-fungible assets from a privileged origin. + /// + /// This new asset class has no assets initially. + /// + /// The origin must conform to `ForceOrigin`. + /// + /// Unlike `create`, no funds are reserved. + /// + /// - `class`: The identifier of the new asset. This must not be currently in use. + /// - `owner`: The owner of this class of assets. The owner has full superuser permissions + /// over this asset, but may later change and configure the permissions using + /// `transfer_ownership` and `set_team`. + /// + /// Emits `ForceCreated` event when successful. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::force_create())] + pub(super) fn force_create( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + owner: ::Source, + free_holding: bool, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let owner = T::Lookup::lookup(owner)?; + + ensure!(!Class::::contains_key(class), Error::::InUse); + + Class::::insert( + class, + ClassDetails { + owner: owner.clone(), + issuer: owner.clone(), + admin: owner.clone(), + freezer: owner.clone(), + total_deposit: Zero::zero(), + free_holding, + instances: 0, + instance_metadatas: 0, + attributes: 0, + is_frozen: false, + }, + ); + Self::deposit_event(Event::ForceCreated(class, owner)); + Ok(()) + } + + /// Destroy a class of fungible assets. + /// + /// The origin must conform to `ForceOrigin` or must be `Signed` and the sender must be the + /// owner of the asset `class`. + /// + /// - `class`: The identifier of the asset class to be destroyed. + /// - `witness`: Information on the instances minted in the asset class. This must be + /// correct. + /// + /// Emits `Destroyed` event when successful. + /// + /// Weight: `O(n + m)` where: + /// - `n = witness.instances` + /// - `m = witness.instance_metdadatas` + /// - `a = witness.attributes` + #[pallet::weight(T::WeightInfo::destroy( + witness.instances, + witness.instance_metadatas, + witness.attributes, + ))] + pub(super) fn destroy( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + witness: DestroyWitness, + ) -> DispatchResult { + let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { + Ok(_) => None, + Err(origin) => Some(ensure_signed(origin)?), + }; + Class::::try_mutate_exists(class, |maybe_details| { + let class_details = maybe_details.take().ok_or(Error::::Unknown)?; + if let Some(check_owner) = maybe_check_owner { + ensure!(class_details.owner == check_owner, Error::::NoPermission); + } + ensure!(class_details.instances == witness.instances, Error::::BadWitness); + ensure!(class_details.instance_metadatas == witness.instance_metadatas, Error::::BadWitness); + ensure!(class_details.attributes == witness.attributes, Error::::BadWitness); + + for (instance, details) in Asset::::drain_prefix(&class) { + Account::::remove((&details.owner, &class, &instance)); + } + InstanceMetadataOf::::remove_prefix(&class); + ClassMetadataOf::::remove(&class); + Attribute::::remove_prefix((&class,)); + T::Currency::unreserve(&class_details.owner, class_details.total_deposit); + + Self::deposit_event(Event::Destroyed(class)); + + // NOTE: could use postinfo to reflect the actual number of accounts/sufficient/approvals + Ok(()) + }) + } + + /// Mint an asset instance of a particular class. + /// + /// The origin must be Signed and the sender must be the Issuer of the asset `class`. + /// + /// - `class`: The class of the asset to be minted. + /// - `instance`: The instance value of the asset to be minted. + /// - `beneficiary`: The initial owner of the minted asset. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::mint())] + pub(super) fn mint( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + owner: ::Source, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let owner = T::Lookup::lookup(owner)?; + + ensure!(!Asset::::contains_key(class, instance), Error::::AlreadyExists); + + Class::::try_mutate(&class, |maybe_class_details| -> DispatchResult { + let class_details = maybe_class_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(class_details.issuer == origin, Error::::NoPermission); + + let instances = class_details.instances.checked_add(1) + .ok_or(ArithmeticError::Overflow)?; + class_details.instances = instances; + + let deposit = match class_details.free_holding { + true => Zero::zero(), + false => T::InstanceDeposit::get(), + }; + T::Currency::reserve(&class_details.owner, deposit)?; + class_details.total_deposit += deposit; + + let owner = owner.clone(); + Account::::insert((&owner, &class, &instance), ()); + let details = InstanceDetails { owner, approved: None, is_frozen: false, deposit}; + Asset::::insert(&class, &instance, details); + Ok(()) + })?; + + Self::deposit_event(Event::Issued(class, instance, owner)); + Ok(()) + } + + /// Destroy a single asset instance. + /// + /// Origin must be Signed and the sender should be the Admin of the asset `class`. + /// + /// - `class`: The class of the asset to be burned. + /// - `instance`: The instance of the asset to be burned. + /// - `check_owner`: If `Some` then the operation will fail with `WrongOwner` unless the + /// asset is owned by this value. + /// + /// Emits `Burned` with the actual amount burned. + /// + /// Weight: `O(1)` + /// Modes: `check_owner.is_some()`. + #[pallet::weight(T::WeightInfo::burn())] + pub(super) fn burn( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + check_owner: Option<::Source>, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let check_owner = check_owner.map(T::Lookup::lookup).transpose()?; + + let owner = Class::::try_mutate(&class, |maybe_class_details| -> Result { + let class_details = maybe_class_details.as_mut().ok_or(Error::::Unknown)?; + let details = Asset::::get(&class, &instance) + .ok_or(Error::::Unknown)?; + let is_permitted = class_details.admin == origin || details.owner == origin; + ensure!(is_permitted, Error::::NoPermission); + ensure!(check_owner.map_or(true, |o| o == details.owner), Error::::WrongOwner); + + // Return the deposit. + T::Currency::unreserve(&class_details.owner, details.deposit); + class_details.total_deposit.saturating_reduce(details.deposit); + class_details.instances.saturating_dec(); + Ok(details.owner) + })?; + + + Asset::::remove(&class, &instance); + Account::::remove((&owner, &class, &instance)); + + Self::deposit_event(Event::Burned(class, instance, owner)); + Ok(()) + } + + /// Move an asset from the sender account to another. + /// + /// Origin must be Signed and the signing account must be either: + /// - the Admin of the asset `class`; + /// - the Owner of the asset `instance`; + /// - the approved delegate for the asset `instance` (in this case, the approval is reset). + /// + /// Arguments: + /// - `class`: The class of the asset to be transferred. + /// - `instance`: The instance of the asset to be transferred. + /// - `dest`: The account to receive ownership of the asset. + /// + /// Emits `Transferred`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::transfer())] + pub(super) fn transfer( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + dest: ::Source, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + ensure!(!class_details.is_frozen, Error::::Frozen); + + let mut details = Asset::::get(&class, &instance).ok_or(Error::::Unknown)?; + ensure!(!details.is_frozen, Error::::Frozen); + if details.owner != origin && class_details.admin != origin { + let approved = details.approved.take().map_or(false, |i| i == origin); + ensure!(approved, Error::::NoPermission); + } + + Account::::remove((&details.owner, &class, &instance)); + Account::::insert((&dest, &class, &instance), ()); + details.owner = dest; + Asset::::insert(&class, &instance, &details); + + Self::deposit_event(Event::Transferred(class, instance, origin, details.owner)); + + Ok(()) + } + + /// Reevaluate the deposits on some assets. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `class`. + /// + /// - `class`: The class of the asset to be frozen. + /// - `instances`: The instances of the asset class whose deposits will be reevaluated. + /// + /// NOTE: This exists as a best-effort function. Any asset instances which are unknown or + /// in the case that the owner account does not have reservable funds to pay for a + /// deposit increase are ignored. Generally the owner isn't going to call this on instances + /// whose existing deposit is less than the refreshed deposit as it would only cost them, + /// so it's of little consequence. + /// + /// It will still return an error in the case that the class is unknown of the signer is + /// not permitted to call it. + /// + /// Weight: `O(instances.len())` + #[pallet::weight(T::WeightInfo::redeposit(instances.len() as u32))] + pub(super) fn redeposit( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + instances: Vec, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + ensure!(class_details.owner == origin, Error::::NoPermission); + let deposit = match class_details.free_holding { + true => Zero::zero(), + false => T::InstanceDeposit::get(), + }; + + let mut successful = Vec::with_capacity(instances.len()); + for instance in instances.into_iter() { + let mut details = match Asset::::get(&class, &instance) { + Some(x) => x, + None => continue, + }; + let old = details.deposit; + if old > deposit { + T::Currency::unreserve(&class_details.owner, old - deposit); + } else if deposit > old { + if T::Currency::reserve(&class_details.owner, deposit - old).is_err() { + // NOTE: No alterations made to class_details in this iteration so far, so + // this is OK to do. + continue + } + } else { + continue + } + class_details.total_deposit.saturating_accrue(deposit); + class_details.total_deposit.saturating_reduce(old); + details.deposit = deposit; + Asset::::insert(&class, &instance, &details); + successful.push(instance); + } + Class::::insert(&class, &class_details); + + Self::deposit_event(Event::::Redeposited(class, successful)); + + Ok(()) + } + + /// Disallow further unprivileged transfer of an asset instance. + /// + /// Origin must be Signed and the sender should be the Freezer of the asset `class`. + /// + /// - `class`: The class of the asset to be frozen. + /// - `instance`: The instance of the asset to be frozen. + /// + /// Emits `Frozen`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::freeze())] + pub(super) fn freeze( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + let mut details = Asset::::get(&class, &instance) + .ok_or(Error::::Unknown)?; + let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + ensure!(class_details.freezer == origin, Error::::NoPermission); + + details.is_frozen = true; + Asset::::insert(&class, &instance, &details); + + Self::deposit_event(Event::::Frozen(class, instance)); + Ok(()) + } + + /// Re-allow unprivileged transfer of an asset instance. + /// + /// Origin must be Signed and the sender should be the Freezer of the asset `class`. + /// + /// - `class`: The class of the asset to be thawed. + /// - `instance`: The instance of the asset to be thawed. + /// + /// Emits `Thawed`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::thaw())] + pub(super) fn thaw( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + let mut details = Asset::::get(&class, &instance) + .ok_or(Error::::Unknown)?; + let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + ensure!(class_details.admin == origin, Error::::NoPermission); + + details.is_frozen = false; + Asset::::insert(&class, &instance, &details); + + Self::deposit_event(Event::::Thawed(class, instance)); + Ok(()) + } + + /// Disallow further unprivileged transfers for a whole asset class. + /// + /// Origin must be Signed and the sender should be the Freezer of the asset `class`. + /// + /// - `class`: The asset class to be frozen. + /// + /// Emits `ClassFrozen`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::freeze_class())] + pub(super) fn freeze_class( + origin: OriginFor, + #[pallet::compact] class: T::ClassId + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + Class::::try_mutate(class, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(&origin == &details.freezer, Error::::NoPermission); + + details.is_frozen = true; + + Self::deposit_event(Event::::ClassFrozen(class)); + Ok(()) + }) + } + + /// Re-allow unprivileged transfers for a whole asset class. + /// + /// Origin must be Signed and the sender should be the Admin of the asset `class`. + /// + /// - `class`: The class to be thawed. + /// + /// Emits `ClassThawed`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::thaw_class())] + pub(super) fn thaw_class( + origin: OriginFor, + #[pallet::compact] class: T::ClassId + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + + Class::::try_mutate(class, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(&origin == &details.admin, Error::::NoPermission); + + details.is_frozen = false; + + Self::deposit_event(Event::::ClassThawed(class)); + Ok(()) + }) + } + + /// Change the Owner of an asset class. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `class`. + /// + /// - `class`: The asset class whose owner should be changed. + /// - `owner`: The new Owner of this asset class. + /// + /// Emits `OwnerChanged`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::transfer_ownership())] + pub(super) fn transfer_ownership( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + owner: ::Source, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let owner = T::Lookup::lookup(owner)?; + + Class::::try_mutate(class, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(&origin == &details.owner, Error::::NoPermission); + if details.owner == owner { + return Ok(()); + } + + // Move the deposit to the new owner. + T::Currency::repatriate_reserved( + &details.owner, + &owner, + details.total_deposit, + Reserved, + )?; + details.owner = owner.clone(); + + Self::deposit_event(Event::OwnerChanged(class, owner)); + Ok(()) + }) + } + + /// Change the Issuer, Admin and Freezer of an asset class. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `class`. + /// + /// - `class`: The asset class whose team should be changed. + /// - `issuer`: The new Issuer of this asset class. + /// - `admin`: The new Admin of this asset class. + /// - `freezer`: The new Freezer of this asset class. + /// + /// Emits `TeamChanged`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::set_team())] + pub(super) fn set_team( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + issuer: ::Source, + admin: ::Source, + freezer: ::Source, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let issuer = T::Lookup::lookup(issuer)?; + let admin = T::Lookup::lookup(admin)?; + let freezer = T::Lookup::lookup(freezer)?; + + Class::::try_mutate(class, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(&origin == &details.owner, Error::::NoPermission); + + details.issuer = issuer.clone(); + details.admin = admin.clone(); + details.freezer = freezer.clone(); + + Self::deposit_event(Event::TeamChanged(class, issuer, admin, freezer)); + Ok(()) + }) + } + + /// Approve an instance to be transferred by a delegated third-party account. + /// + /// Origin must be Signed and must be the owner of the asset `instance`. + /// + /// - `class`: The class of the asset to be approved for delegated transfer. + /// - `instance`: The instance of the asset to be approved for delegated transfer. + /// - `delegate`: The account to delegate permission to transfer the asset. + /// + /// Emits `ApprovedTransfer` on success. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::approve_transfer())] + pub(super) fn approve_transfer( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + delegate: ::Source, + ) -> DispatchResult { + let maybe_check: Option = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + + let delegate = T::Lookup::lookup(delegate)?; + + let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + let mut details = Asset::::get(&class, &instance) + .ok_or(Error::::Unknown)?; + + if let Some(check) = maybe_check { + let permitted = &check == &class_details.admin || &check == &details.owner; + ensure!(permitted, Error::::NoPermission); + } + + details.approved = Some(delegate); + Asset::::insert(&class, &instance, &details); + + let delegate = details.approved.expect("set as Some above; qed"); + Self::deposit_event(Event::ApprovedTransfer(class, instance, details.owner, delegate)); + + Ok(()) + } + + /// Cancel the prior approval for the transfer of an asset by a delegate. + /// + /// Origin must be either: + /// - the `Force` origin; + /// - `Signed` with the signer being the Admin of the asset `class`; + /// - `Signed` with the signer being the Owner of the asset `instance`; + /// + /// Arguments: + /// - `class`: The class of the asset of whose approval will be cancelled. + /// - `instance`: The instance of the asset of whose approval will be cancelled. + /// - `maybe_check_delegate`: If `Some` will ensure that the given account is the one to + /// which permission of transfer is delegated. + /// + /// Emits `ApprovalCancelled` on success. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::cancel_approval())] + pub(super) fn cancel_approval( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + maybe_check_delegate: Option<::Source>, + ) -> DispatchResult { + let maybe_check: Option = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; + + let class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + let mut details = Asset::::get(&class, &instance) + .ok_or(Error::::Unknown)?; + if let Some(check) = maybe_check { + let permitted = &check == &class_details.admin || &check == &details.owner; + ensure!(permitted, Error::::NoPermission); + } + let maybe_check_delegate = maybe_check_delegate.map(T::Lookup::lookup).transpose()?; + let old = details.approved.take().ok_or(Error::::NoDelegate)?; + if let Some(check_delegate) = maybe_check_delegate { + ensure!(check_delegate == old, Error::::WrongDelegate); + } + + Asset::::insert(&class, &instance, &details); + Self::deposit_event(Event::ApprovalCancelled(class, instance, details.owner, old)); + + Ok(()) + } + + /// Alter the attributes of a given asset. + /// + /// Origin must be `ForceOrigin`. + /// + /// - `class`: The identifier of the asset. + /// - `owner`: The new Owner of this asset. + /// - `issuer`: The new Issuer of this asset. + /// - `admin`: The new Admin of this asset. + /// - `freezer`: The new Freezer of this asset. + /// - `free_holding`: Whether a deposit is taken for holding an instance of this asset + /// class. + /// - `is_frozen`: Whether this asset class is frozen except for permissioned/admin + /// instructions. + /// + /// Emits `AssetStatusChanged` with the identity of the asset. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::force_asset_status())] + pub(super) fn force_asset_status( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + owner: ::Source, + issuer: ::Source, + admin: ::Source, + freezer: ::Source, + free_holding: bool, + is_frozen: bool, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + + Class::::try_mutate(class, |maybe_asset| { + let mut asset = maybe_asset.take().ok_or(Error::::Unknown)?; + asset.owner = T::Lookup::lookup(owner)?; + asset.issuer = T::Lookup::lookup(issuer)?; + asset.admin = T::Lookup::lookup(admin)?; + asset.freezer = T::Lookup::lookup(freezer)?; + asset.free_holding = free_holding; + asset.is_frozen = is_frozen; + *maybe_asset = Some(asset); + + Self::deposit_event(Event::AssetStatusChanged(class)); + Ok(()) + }) + } + + /// Set an attribute for an asset class or instance. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// asset `class`. + /// + /// If the origin is Signed, then funds of signer are reserved according to the formula: + /// `MetadataDepositBase + DepositPerByte * (key.len + value.len)` taking into + /// account any already reserved funds. + /// + /// - `class`: The identifier of the asset class whose instance's metadata to set. + /// - `maybe_instance`: The identifier of the asset instance whose metadata to set. + /// - `key`: The key of the attribute. + /// - `value`: The value to which to set the attribute. + /// + /// Emits `AttributeSet`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::set_attribute())] + pub(super) fn set_attribute( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + maybe_instance: Option, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &class_details.owner, Error::::NoPermission); + } + let maybe_is_frozen = match maybe_instance { + None => ClassMetadataOf::::get(class).map(|v| v.is_frozen), + Some(instance) => + InstanceMetadataOf::::get(class, instance).map(|v| v.is_frozen), + }; + ensure!(!maybe_is_frozen.unwrap_or(false), Error::::Frozen); + + let attribute = Attribute::::get((class, maybe_instance, &key)); + if attribute.is_none() { + class_details.attributes.saturating_inc(); + } + let old_deposit = attribute.map_or(Zero::zero(), |m| m.1); + class_details.total_deposit.saturating_reduce(old_deposit); + let mut deposit = Zero::zero(); + if !class_details.free_holding && maybe_check_owner.is_some() { + deposit = T::DepositPerByte::get() + .saturating_mul(((key.len() + value.len()) as u32).into()) + .saturating_add(T::AttributeDepositBase::get()); + } + class_details.total_deposit.saturating_accrue(deposit); + if deposit > old_deposit { + T::Currency::reserve(&class_details.owner, deposit - old_deposit)?; + } else if deposit < old_deposit { + T::Currency::unreserve(&class_details.owner, old_deposit - deposit); + } + + Attribute::::insert((&class, maybe_instance, &key), (&value, deposit)); + Class::::insert(class, &class_details); + Self::deposit_event(Event::AttributeSet(class, maybe_instance, key, value)); + Ok(()) + } + + /// Set an attribute for an asset class or instance. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// asset `class`. + /// + /// If the origin is Signed, then funds of signer are reserved according to the formula: + /// `MetadataDepositBase + DepositPerByte * (key.len + value.len)` taking into + /// account any already reserved funds. + /// + /// - `class`: The identifier of the asset class whose instance's metadata to set. + /// - `instance`: The identifier of the asset instance whose metadata to set. + /// - `key`: The key of the attribute. + /// - `value`: The value to which to set the attribute. + /// + /// Emits `AttributeSet`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::clear_attribute())] + pub(super) fn clear_attribute( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + maybe_instance: Option, + key: BoundedVec, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + let mut class_details = Class::::get(&class).ok_or(Error::::Unknown)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &class_details.owner, Error::::NoPermission); + } + let maybe_is_frozen = match maybe_instance { + None => ClassMetadataOf::::get(class).map(|v| v.is_frozen), + Some(instance) => + InstanceMetadataOf::::get(class, instance).map(|v| v.is_frozen), + }; + ensure!(!maybe_is_frozen.unwrap_or(false), Error::::Frozen); + + if let Some((_, deposit)) = Attribute::::take((class, maybe_instance, &key)) { + class_details.attributes.saturating_dec(); + class_details.total_deposit.saturating_reduce(deposit); + T::Currency::unreserve(&class_details.owner, deposit); + Class::::insert(class, &class_details); + Self::deposit_event(Event::AttributeCleared(class, maybe_instance, key)); + } + Ok(()) + } + + /// Set the metadata for an asset instance. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// asset `class`. + /// + /// If the origin is Signed, then funds of signer are reserved according to the formula: + /// `MetadataDepositBase + DepositPerByte * data.len` taking into + /// account any already reserved funds. + /// + /// - `class`: The identifier of the asset class whose instance's metadata to set. + /// - `instance`: The identifier of the asset instance whose metadata to set. + /// - `data`: The general information of this asset. Limited in length by `StringLimit`. + /// - `is_frozen`: Whether the metadata should be frozen against further changes. + /// + /// Emits `MetadataSet`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::set_metadata())] + pub(super) fn set_metadata( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + data: BoundedVec, + is_frozen: bool, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + let mut class_details = Class::::get(&class) + .ok_or(Error::::Unknown)?; + + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &class_details.owner, Error::::NoPermission); + } + + InstanceMetadataOf::::try_mutate_exists(class, instance, |metadata| { + let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); + ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + + if metadata.is_none() { + class_details.instance_metadatas.saturating_inc(); + } + let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); + class_details.total_deposit.saturating_reduce(old_deposit); + let mut deposit = Zero::zero(); + if !class_details.free_holding && maybe_check_owner.is_some() { + deposit = T::DepositPerByte::get() + .saturating_mul(((data.len()) as u32).into()) + .saturating_add(T::MetadataDepositBase::get()); + } + if deposit > old_deposit { + T::Currency::reserve(&class_details.owner, deposit - old_deposit)?; + } else if deposit < old_deposit { + T::Currency::unreserve(&class_details.owner, old_deposit - deposit); + } + class_details.total_deposit.saturating_accrue(deposit); + + *metadata = Some(InstanceMetadata { + deposit, + data: data.clone(), + is_frozen, + }); + + Class::::insert(&class, &class_details); + Self::deposit_event(Event::MetadataSet(class, instance, data, is_frozen)); + Ok(()) + }) + } + + /// Clear the metadata for an asset instance. + /// + /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the + /// asset `instance`. + /// + /// Any deposit is freed for the asset class owner. + /// + /// - `class`: The identifier of the asset class whose instance's metadata to clear. + /// - `instance`: The identifier of the asset instance whose metadata to clear. + /// + /// Emits `MetadataCleared`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::clear_metadata())] + pub(super) fn clear_metadata( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + #[pallet::compact] instance: T::InstanceId, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + let mut class_details = Class::::get(&class) + .ok_or(Error::::Unknown)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &class_details.owner, Error::::NoPermission); + } + + InstanceMetadataOf::::try_mutate_exists(class, instance, |metadata| { + let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); + ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + + if metadata.is_some() { + class_details.instance_metadatas.saturating_dec(); + } + let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; + T::Currency::unreserve(&class_details.owner, deposit); + class_details.total_deposit.saturating_reduce(deposit); + + Class::::insert(&class, &class_details); + Self::deposit_event(Event::MetadataCleared(class, instance)); + Ok(()) + }) + } + + /// Set the metadata for an asset class. + /// + /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of + /// the asset `class`. + /// + /// If the origin is `Signed`, then funds of signer are reserved according to the formula: + /// `MetadataDepositBase + DepositPerByte * data.len` taking into + /// account any already reserved funds. + /// + /// - `class`: The identifier of the asset whose metadata to update. + /// - `data`: The general information of this asset. Limited in length by `StringLimit`. + /// - `is_frozen`: Whether the metadata should be frozen against further changes. + /// + /// Emits `ClassMetadataSet`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::set_class_metadata())] + pub(super) fn set_class_metadata( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + data: BoundedVec, + is_frozen: bool, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + let mut details = Class::::get(&class).ok_or(Error::::Unknown)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &details.owner, Error::::NoPermission); + } + + ClassMetadataOf::::try_mutate_exists(class, |metadata| { + let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); + ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + + let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); + details.total_deposit.saturating_reduce(old_deposit); + let mut deposit = Zero::zero(); + if maybe_check_owner.is_some() && !details.free_holding { + deposit = T::DepositPerByte::get() + .saturating_mul(((data.len()) as u32).into()) + .saturating_add(T::MetadataDepositBase::get()); + } + if deposit > old_deposit { + T::Currency::reserve(&details.owner, deposit - old_deposit)?; + } else if deposit < old_deposit { + T::Currency::unreserve(&details.owner, old_deposit - deposit); + } + details.total_deposit.saturating_accrue(deposit); + + Class::::insert(&class, details); + + *metadata = Some(ClassMetadata { + deposit, + data: data.clone(), + is_frozen, + }); + + Self::deposit_event(Event::ClassMetadataSet(class, data, is_frozen)); + Ok(()) + }) + } + + /// Clear the metadata for an asset class. + /// + /// Origin must be either `ForceOrigin` or `Signed` and the sender should be the Owner of + /// the asset `class`. + /// + /// Any deposit is freed for the asset class owner. + /// + /// - `class`: The identifier of the asset class whose metadata to clear. + /// + /// Emits `ClassMetadataCleared`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::clear_class_metadata())] + pub(super) fn clear_class_metadata( + origin: OriginFor, + #[pallet::compact] class: T::ClassId, + ) -> DispatchResult { + let maybe_check_owner = T::ForceOrigin::try_origin(origin) + .map(|_| None) + .or_else(|origin| ensure_signed(origin).map(Some))?; + + let details = Class::::get(&class).ok_or(Error::::Unknown)?; + if let Some(check_owner) = &maybe_check_owner { + ensure!(check_owner == &details.owner, Error::::NoPermission); + } + + ClassMetadataOf::::try_mutate_exists(class, |metadata| { + let was_frozen = metadata.as_ref().map_or(false, |m| m.is_frozen); + ensure!(maybe_check_owner.is_none() || !was_frozen, Error::::Frozen); + + let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; + T::Currency::unreserve(&details.owner, deposit); + Self::deposit_event(Event::ClassMetadataCleared(class)); + Ok(()) + }) + } + } +} diff --git a/frame/uniques/src/mock.rs b/frame/uniques/src/mock.rs new file mode 100644 index 0000000000000..1040821d0d886 --- /dev/null +++ b/frame/uniques/src/mock.rs @@ -0,0 +1,119 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for Assets pallet. + +use super::*; +use crate as pallet_uniques; + +use sp_core::H256; +use sp_runtime::{traits::{BlakeTwo256, IdentityLookup}, testing::Header}; +use frame_support::{parameter_types, construct_runtime}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} +impl frame_system::Config for Test { + type BaseCallFilter = (); + type BlockWeights = (); + type BlockLength = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); +} + +parameter_types! { + pub const ClassDeposit: u64 = 2; + pub const InstanceDeposit: u64 = 1; + pub const KeyLimit: u32 = 50; + pub const ValueLimit: u32 = 50; + pub const StringLimit: u32 = 50; + pub const MetadataDepositBase: u64 = 1; + pub const AttributeDepositBase: u64 = 1; + pub const MetadataDepositPerByte: u64 = 1; +} + +impl Config for Test { + type Event = Event; + type ClassId = u32; + type InstanceId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type ClassDeposit = ClassDeposit; + type InstanceDeposit = InstanceDeposit; + type MetadataDepositBase = MetadataDepositBase; + type AttributeDepositBase = AttributeDepositBase; + type DepositPerByte = MetadataDepositPerByte; + type StringLimit = StringLimit; + type KeyLimit = KeyLimit; + type ValueLimit = ValueLimit; + type WeightInfo = (); +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/frame/uniques/src/tests.rs b/frame/uniques/src/tests.rs new file mode 100644 index 0000000000000..4673ff71f8ed9 --- /dev/null +++ b/frame/uniques/src/tests.rs @@ -0,0 +1,527 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for Uniques pallet. + +use super::*; +use crate::mock::*; +use sp_std::convert::TryInto; +use frame_support::{assert_ok, assert_noop, traits::Currency}; +use pallet_balances::Error as BalancesError; + +fn assets() -> Vec<(u64, u32, u32)> { + let mut r: Vec<_> = Account::::iter().map(|x| x.0).collect(); + r.sort(); + let mut s: Vec<_> = Asset::::iter().map(|x| (x.2.owner, x.0, x.1)).collect(); + s.sort(); + assert_eq!(r, s); + for class in Asset::::iter() + .map(|x| x.0) + .scan(None, |s, item| if s.map_or(false, |last| last == item) { + *s = Some(item); + Some(None) + } else { + Some(Some(item)) + } + ).filter_map(|item| item) + { + let details = Class::::get(class).unwrap(); + let instances = Asset::::iter_prefix(class).count() as u32; + assert_eq!(details.instances, instances); + } + r +} + +macro_rules! bvec { + ($( $x:tt )*) => { + vec![$( $x )*].try_into().unwrap() + } +} + +fn attributes(class: u32) -> Vec<(Option, Vec, Vec)> { + let mut s: Vec<_> = Attribute::::iter_prefix((class,)) + .map(|(k, v)| (k.0, k.1.into(), v.0.into())) + .collect(); + s.sort(); + s +} + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with(|| { + assert_eq!(assets(), vec![]); + }); +} + +#[test] +fn basic_minting_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_eq!(assets(), vec![(1, 0, 42)]); + + assert_ok!(Uniques::force_create(Origin::root(), 1, 2, true)); + assert_ok!(Uniques::mint(Origin::signed(2), 1, 69, 1)); + assert_eq!(assets(), vec![(1, 0, 42), (1, 1, 69)]); + }); +} + +#[test] +fn lifecycle_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_eq!(Balances::reserved_balance(&1), 2); + + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0, 0], false)); + assert_eq!(Balances::reserved_balance(&1), 5); + assert!(ClassMetadataOf::::contains_key(0)); + + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 10)); + assert_eq!(Balances::reserved_balance(&1), 6); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 69, 20)); + assert_eq!(Balances::reserved_balance(&1), 7); + assert_eq!(assets(), vec![(10, 0, 42), (20, 0, 69)]); + assert_eq!(Class::::get(0).unwrap().instances, 2); + assert_eq!(Class::::get(0).unwrap().instance_metadatas, 0); + + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![42, 42], false)); + assert_eq!(Balances::reserved_balance(&1), 10); + assert!(InstanceMetadataOf::::contains_key(0, 42)); + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 69, bvec![69, 69], false)); + assert_eq!(Balances::reserved_balance(&1), 13); + assert!(InstanceMetadataOf::::contains_key(0, 69)); + + let w = Class::::get(0).unwrap().destroy_witness(); + assert_eq!(w.instances, 2); + assert_eq!(w.instance_metadatas, 2); + assert_ok!(Uniques::destroy(Origin::signed(1), 0, w)); + assert_eq!(Balances::reserved_balance(&1), 0); + + assert!(!Class::::contains_key(0)); + assert!(!Asset::::contains_key(0, 42)); + assert!(!Asset::::contains_key(0, 69)); + assert!(!ClassMetadataOf::::contains_key(0)); + assert!(!InstanceMetadataOf::::contains_key(0, 42)); + assert!(!InstanceMetadataOf::::contains_key(0, 69)); + assert_eq!(assets(), vec![]); + }); +} + +#[test] +fn destroy_with_bad_witness_should_not_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + + let w = Class::::get(0).unwrap().destroy_witness(); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_noop!(Uniques::destroy(Origin::signed(1), 0, w), Error::::BadWitness); + }); +} + +#[test] +fn mint_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_eq!(Uniques::owner(0, 42).unwrap(), 1); + assert_eq!(assets(), vec![(1, 0, 42)]); + }); +} + +#[test] +fn transfer_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 2)); + + assert_ok!(Uniques::transfer(Origin::signed(2), 0, 42, 3)); + assert_eq!(assets(), vec![(3, 0, 42)]); + assert_noop!(Uniques::transfer(Origin::signed(2), 0, 42, 4), Error::::NoPermission); + + assert_ok!(Uniques::approve_transfer(Origin::signed(3), 0, 42, 2)); + assert_ok!(Uniques::transfer(Origin::signed(2), 0, 42, 4)); + }); +} + +#[test] +fn freezing_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_ok!(Uniques::freeze(Origin::signed(1), 0, 42)); + assert_noop!(Uniques::transfer(Origin::signed(1), 0, 42, 2), Error::::Frozen); + + assert_ok!(Uniques::thaw(Origin::signed(1), 0, 42)); + assert_ok!(Uniques::freeze_class(Origin::signed(1), 0)); + assert_noop!(Uniques::transfer(Origin::signed(1), 0, 42, 2), Error::::Frozen); + + assert_ok!(Uniques::thaw_class(Origin::signed(1), 0)); + assert_ok!(Uniques::transfer(Origin::signed(1), 0, 42, 2)); + }); +} + +#[test] +fn origin_guards_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_noop!(Uniques::transfer_ownership(Origin::signed(2), 0, 2), Error::::NoPermission); + assert_noop!(Uniques::set_team(Origin::signed(2), 0, 2, 2, 2), Error::::NoPermission); + assert_noop!(Uniques::freeze(Origin::signed(2), 0, 42), Error::::NoPermission); + assert_noop!(Uniques::thaw(Origin::signed(2), 0, 42), Error::::NoPermission); + assert_noop!(Uniques::mint(Origin::signed(2), 0, 69, 2), Error::::NoPermission); + assert_noop!(Uniques::burn(Origin::signed(2), 0, 42, None), Error::::NoPermission); + let w = Class::::get(0).unwrap().destroy_witness(); + assert_noop!(Uniques::destroy(Origin::signed(2), 0, w), Error::::NoPermission); + }); +} + +#[test] +fn transfer_owner_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + Balances::make_free_balance_be(&3, 100); + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::transfer_ownership(Origin::signed(1), 0, 2)); + assert_eq!(Balances::total_balance(&1), 98); + assert_eq!(Balances::total_balance(&2), 102); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Balances::reserved_balance(&2), 2); + + assert_noop!(Uniques::transfer_ownership(Origin::signed(1), 0, 1), Error::::NoPermission); + + // Mint and set metadata now and make sure that deposit gets transferred back. + assert_ok!(Uniques::set_class_metadata(Origin::signed(2), 0, bvec![0u8; 20], false)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_ok!(Uniques::set_metadata(Origin::signed(2), 0, 42, bvec![0u8; 20], false)); + assert_ok!(Uniques::transfer_ownership(Origin::signed(2), 0, 3)); + assert_eq!(Balances::total_balance(&2), 57); + assert_eq!(Balances::total_balance(&3), 145); + assert_eq!(Balances::reserved_balance(&2), 0); + assert_eq!(Balances::reserved_balance(&3), 45); + }); +} + +#[test] +fn set_team_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::set_team(Origin::signed(1), 0, 2, 3, 4)); + + assert_ok!(Uniques::mint(Origin::signed(2), 0, 42, 2)); + assert_ok!(Uniques::freeze(Origin::signed(4), 0, 42)); + assert_ok!(Uniques::thaw(Origin::signed(3), 0, 42)); + assert_ok!(Uniques::transfer(Origin::signed(3), 0, 42, 3)); + assert_ok!(Uniques::burn(Origin::signed(3), 0, 42, None)); + }); +} + +#[test] +fn set_class_metadata_should_work() { + new_test_ext().execute_with(|| { + // Cannot add metadata to unknown asset + assert_noop!( + Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 20], false), + Error::::Unknown, + ); + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, false)); + // Cannot add metadata to unowned asset + assert_noop!( + Uniques::set_class_metadata(Origin::signed(2), 0, bvec![0u8; 20], false), + Error::::NoPermission, + ); + + // Successfully add metadata and take deposit + Balances::make_free_balance_be(&1, 30); + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 20], false)); + assert_eq!(Balances::free_balance(&1), 9); + assert!(ClassMetadataOf::::contains_key(0)); + + // Force origin works, too. + assert_ok!(Uniques::set_class_metadata(Origin::root(), 0, bvec![0u8; 18], false)); + + // Update deposit + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 15], false)); + assert_eq!(Balances::free_balance(&1), 14); + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 25], false)); + assert_eq!(Balances::free_balance(&1), 4); + + // Cannot over-reserve + assert_noop!( + Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 40], false), + BalancesError::::InsufficientBalance, + ); + + // Can't set or clear metadata once frozen + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 15], true)); + assert_noop!( + Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0u8; 15], false), + Error::::Frozen, + ); + assert_noop!(Uniques::clear_class_metadata(Origin::signed(1), 0), Error::::Frozen); + + // Clear Metadata + assert_ok!(Uniques::set_class_metadata(Origin::root(), 0, bvec![0u8; 15], false)); + assert_noop!(Uniques::clear_class_metadata(Origin::signed(2), 0), Error::::NoPermission); + assert_noop!(Uniques::clear_class_metadata(Origin::signed(1), 1), Error::::Unknown); + assert_ok!(Uniques::clear_class_metadata(Origin::signed(1), 0)); + assert!(!ClassMetadataOf::::contains_key(0)); + }); +} + +#[test] +fn set_instance_metadata_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 30); + + // Cannot add metadata to unknown asset + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, false)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + // Cannot add metadata to unowned asset + assert_noop!( + Uniques::set_metadata(Origin::signed(2), 0, 42, bvec![0u8; 20], false), + Error::::NoPermission, + ); + + // Successfully add metadata and take deposit + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0u8; 20], false)); + assert_eq!(Balances::free_balance(&1), 8); + assert!(InstanceMetadataOf::::contains_key(0, 42)); + + // Force origin works, too. + assert_ok!(Uniques::set_metadata(Origin::root(), 0, 42, bvec![0u8; 18], false)); + + // Update deposit + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0u8; 15], false)); + assert_eq!(Balances::free_balance(&1), 13); + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0u8; 25], false)); + assert_eq!(Balances::free_balance(&1), 3); + + // Cannot over-reserve + assert_noop!( + Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0u8; 40], false), + BalancesError::::InsufficientBalance, + ); + + // Can't set or clear metadata once frozen + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0u8; 15], true)); + assert_noop!( + Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0u8; 15], false), + Error::::Frozen, + ); + assert_noop!(Uniques::clear_metadata(Origin::signed(1), 0, 42), Error::::Frozen); + + // Clear Metadata + assert_ok!(Uniques::set_metadata(Origin::root(), 0, 42, bvec![0u8; 15], false)); + assert_noop!(Uniques::clear_metadata(Origin::signed(2), 0, 42), Error::::NoPermission); + assert_noop!(Uniques::clear_metadata(Origin::signed(1), 1, 42), Error::::Unknown); + assert_ok!(Uniques::clear_metadata(Origin::signed(1), 0, 42)); + assert!(!InstanceMetadataOf::::contains_key(0, 42)); + }); +} + +#[test] +fn set_attribute_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, false)); + + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, None, bvec![0], bvec![0])); + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, Some(0), bvec![0], bvec![0])); + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, Some(0), bvec![1], bvec![0])); + assert_eq!(attributes(0), vec![ + (None, bvec![0], bvec![0]), + (Some(0), bvec![0], bvec![0]), + (Some(0), bvec![1], bvec![0]), + ]); + assert_eq!(Balances::reserved_balance(1), 9); + + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, None, bvec![0], bvec![0; 10])); + assert_eq!(attributes(0), vec![ + (None, bvec![0], bvec![0; 10]), + (Some(0), bvec![0], bvec![0]), + (Some(0), bvec![1], bvec![0]), + ]); + assert_eq!(Balances::reserved_balance(1), 18); + + assert_ok!(Uniques::clear_attribute(Origin::signed(1), 0, Some(0), bvec![1])); + assert_eq!(attributes(0), vec![ + (None, bvec![0], bvec![0; 10]), + (Some(0), bvec![0], bvec![0]), + ]); + assert_eq!(Balances::reserved_balance(1), 15); + + let w = Class::::get(0).unwrap().destroy_witness(); + assert_ok!(Uniques::destroy(Origin::signed(1), 0, w)); + assert_eq!(attributes(0), vec![]); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn set_attribute_should_respect_freeze() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, false)); + + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, None, bvec![0], bvec![0])); + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, Some(0), bvec![0], bvec![0])); + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, Some(1), bvec![0], bvec![0])); + assert_eq!(attributes(0), vec![ + (None, bvec![0], bvec![0]), + (Some(0), bvec![0], bvec![0]), + (Some(1), bvec![0], bvec![0]), + ]); + assert_eq!(Balances::reserved_balance(1), 9); + + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![], true)); + let e = Error::::Frozen; + assert_noop!(Uniques::set_attribute(Origin::signed(1), 0, None, bvec![0], bvec![0]), e); + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, Some(0), bvec![0], bvec![1])); + + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 0, bvec![], true)); + let e = Error::::Frozen; + assert_noop!(Uniques::set_attribute(Origin::signed(1), 0, Some(0), bvec![0], bvec![1]), e); + assert_ok!(Uniques::set_attribute(Origin::signed(1), 0, Some(1), bvec![0], bvec![1])); + }); +} + +#[test] +fn force_asset_status_should_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, false)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 69, 2)); + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0; 20], false)); + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0; 20], false)); + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 69, bvec![0; 20], false)); + assert_eq!(Balances::reserved_balance(1), 65); + + //force asset status to be free holding + assert_ok!(Uniques::force_asset_status(Origin::root(), 0, 1, 1, 1, 1, true, false)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 142, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 169, 2)); + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 142, bvec![0; 20], false)); + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 169, bvec![0; 20], false)); + assert_eq!(Balances::reserved_balance(1), 65); + + assert_ok!(Uniques::redeposit(Origin::signed(1), 0, bvec![0, 42, 50, 69, 100])); + assert_eq!(Balances::reserved_balance(1), 63); + + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 42, bvec![0; 20], false)); + assert_eq!(Balances::reserved_balance(1), 42); + + assert_ok!(Uniques::set_metadata(Origin::signed(1), 0, 69, bvec![0; 20], false)); + assert_eq!(Balances::reserved_balance(1), 21); + + assert_ok!(Uniques::set_class_metadata(Origin::signed(1), 0, bvec![0; 20], false)); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn burn_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, false)); + assert_ok!(Uniques::set_team(Origin::signed(1), 0, 2, 3, 4)); + + assert_noop!(Uniques::burn(Origin::signed(5), 0, 42, Some(5)), Error::::Unknown); + + assert_ok!(Uniques::mint(Origin::signed(2), 0, 42, 5)); + assert_ok!(Uniques::mint(Origin::signed(2), 0, 69, 5)); + assert_eq!(Balances::reserved_balance(1), 2); + + assert_noop!(Uniques::burn(Origin::signed(0), 0, 42, None), Error::::NoPermission); + assert_noop!(Uniques::burn(Origin::signed(5), 0, 42, Some(6)), Error::::WrongOwner); + + assert_ok!(Uniques::burn(Origin::signed(5), 0, 42, Some(5))); + assert_ok!(Uniques::burn(Origin::signed(3), 0, 69, Some(5))); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn approval_lifecycle_works() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 2)); + assert_ok!(Uniques::approve_transfer(Origin::signed(2), 0, 42, 3)); + assert_ok!(Uniques::transfer(Origin::signed(3), 0, 42, 4)); + assert_noop!(Uniques::transfer(Origin::signed(3), 0, 42, 3), Error::::NoPermission); + assert!(Asset::::get(0, 42).unwrap().approved.is_none()); + + assert_ok!(Uniques::approve_transfer(Origin::signed(4), 0, 42, 2)); + assert_ok!(Uniques::transfer(Origin::signed(2), 0, 42, 2)); + }); +} + +#[test] +fn cancel_approval_works() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 2)); + + assert_ok!(Uniques::approve_transfer(Origin::signed(2), 0, 42, 3)); + assert_noop!(Uniques::cancel_approval(Origin::signed(2), 1, 42, None), Error::::Unknown); + assert_noop!(Uniques::cancel_approval(Origin::signed(2), 0, 43, None), Error::::Unknown); + assert_noop!(Uniques::cancel_approval(Origin::signed(3), 0, 42, None), Error::::NoPermission); + assert_noop!(Uniques::cancel_approval(Origin::signed(2), 0, 42, Some(4)), Error::::WrongDelegate); + + assert_ok!(Uniques::cancel_approval(Origin::signed(2), 0, 42, Some(3))); + assert_noop!(Uniques::cancel_approval(Origin::signed(2), 0, 42, None), Error::::NoDelegate); + }); +} + +#[test] +fn cancel_approval_works_with_admin() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 2)); + + assert_ok!(Uniques::approve_transfer(Origin::signed(2), 0, 42, 3)); + assert_noop!(Uniques::cancel_approval(Origin::signed(1), 1, 42, None), Error::::Unknown); + assert_noop!(Uniques::cancel_approval(Origin::signed(1), 0, 43, None), Error::::Unknown); + assert_noop!(Uniques::cancel_approval(Origin::signed(1), 0, 42, Some(4)), Error::::WrongDelegate); + + assert_ok!(Uniques::cancel_approval(Origin::signed(1), 0, 42, Some(3))); + assert_noop!(Uniques::cancel_approval(Origin::signed(1), 0, 42, None), Error::::NoDelegate); + }); +} + +#[test] +fn cancel_approval_works_with_force() { + new_test_ext().execute_with(|| { + assert_ok!(Uniques::force_create(Origin::root(), 0, 1, true)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 42, 2)); + + assert_ok!(Uniques::approve_transfer(Origin::signed(2), 0, 42, 3)); + assert_noop!(Uniques::cancel_approval(Origin::root(), 1, 42, None), Error::::Unknown); + assert_noop!(Uniques::cancel_approval(Origin::root(), 0, 43, None), Error::::Unknown); + assert_noop!(Uniques::cancel_approval(Origin::root(), 0, 42, Some(4)), Error::::WrongDelegate); + + assert_ok!(Uniques::cancel_approval(Origin::root(), 0, 42, Some(3))); + assert_noop!(Uniques::cancel_approval(Origin::root(), 0, 42, None), Error::::NoDelegate); + }); +} diff --git a/frame/uniques/src/types.rs b/frame/uniques/src/types.rs new file mode 100644 index 0000000000000..45b571aa7de2c --- /dev/null +++ b/frame/uniques/src/types.rs @@ -0,0 +1,118 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Various basic types for use in the assets pallet. + +use super::*; +use frame_support::{traits::Get, BoundedVec}; + +pub(super) type DepositBalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] +pub struct ClassDetails< + AccountId, + DepositBalance, +> { + /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. + pub(super) owner: AccountId, + /// Can mint tokens. + pub(super) issuer: AccountId, + /// Can thaw tokens, force transfers and burn tokens from any account. + pub(super) admin: AccountId, + /// Can freeze tokens. + pub(super) freezer: AccountId, + /// The total balance deposited for the all storage associated with this asset class. Used by + /// `destroy`. + pub(super) total_deposit: DepositBalance, + /// If `true`, then no deposit is needed to hold instances of this class. + pub(super) free_holding: bool, + /// The total number of outstanding instances of this asset class. + pub(super) instances: u32, + /// The total number of outstanding instance metadata of this asset class. + pub(super) instance_metadatas: u32, + /// The total number of attributes for this asset class. + pub(super) attributes: u32, + /// Whether the asset is frozen for non-admin transfers. + pub(super) is_frozen: bool, +} + +/// Witness data for the destroy transactions. +#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] +pub struct DestroyWitness { + /// The total number of outstanding instances of this asset class. + #[codec(compact)] + pub(super) instances: u32, + /// The total number of outstanding instance metadata of this asset class. + #[codec(compact)] + pub(super) instance_metadatas: u32, + #[codec(compact)] + /// The total number of attributes for this asset class. + pub(super) attributes: u32, +} + +impl ClassDetails { + pub fn destroy_witness(&self) -> DestroyWitness { + DestroyWitness { + instances: self.instances, + instance_metadatas: self.instance_metadatas, + attributes: self.attributes, + } + } +} + +/// Information concerning the ownership of a single unique asset. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] +pub struct InstanceDetails { + /// The owner of this asset. + pub(super) owner: AccountId, + /// The approved transferrer of this asset, if one is set. + pub(super) approved: Option, + /// Whether the asset can be transferred or not. + pub(super) is_frozen: bool, + /// The amount held in the pallet's default account for this asset. Free-hold assets will have + /// this as zero. + pub(super) deposit: DepositBalance, +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] +pub struct ClassMetadata> { + /// The balance deposited for this metadata. + /// + /// This pays for the data stored in this struct. + pub(super) deposit: DepositBalance, + /// General information concerning this asset. Limited in length by `StringLimit`. This will + /// generally be either a JSON dump or the hash of some JSON which can be found on a + /// hash-addressable global publication system such as IPFS. + pub(super) data: BoundedVec, + /// Whether the asset metadata may be changed by a non Force origin. + pub(super) is_frozen: bool, +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] +pub struct InstanceMetadata> { + /// The balance deposited for this metadata. + /// + /// This pays for the data stored in this struct. + pub(super) deposit: DepositBalance, + /// General information concerning this asset. Limited in length by `StringLimit`. This will + /// generally be either a JSON dump or the hash of some JSON which can be found on a + /// hash-addressable global publication system such as IPFS. + pub(super) data: BoundedVec, + /// Whether the asset metadata may be changed by a non Force origin. + pub(super) is_frozen: bool, +} diff --git a/frame/uniques/src/weights.rs b/frame/uniques/src/weights.rs new file mode 100644 index 0000000000000..9272ae6026a9f --- /dev/null +++ b/frame/uniques/src/weights.rs @@ -0,0 +1,326 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_uniques +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-05-24, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_uniques +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/uniques/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_uniques. +pub trait WeightInfo { + fn create() -> Weight; + fn force_create() -> Weight; + fn destroy(n: u32, m: u32, a: u32, ) -> Weight; + fn mint() -> Weight; + fn burn() -> Weight; + fn transfer() -> Weight; + fn redeposit(i: u32, ) -> Weight; + fn freeze() -> Weight; + fn thaw() -> Weight; + fn freeze_class() -> Weight; + fn thaw_class() -> Weight; + fn transfer_ownership() -> Weight; + fn set_team() -> Weight; + fn force_asset_status() -> Weight; + fn set_attribute() -> Weight; + fn clear_attribute() -> Weight; + fn set_metadata() -> Weight; + fn clear_metadata() -> Weight; + fn set_class_metadata() -> Weight; + fn clear_class_metadata() -> Weight; + fn approve_transfer() -> Weight; + fn cancel_approval() -> Weight; +} + +/// Weights for pallet_uniques using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn create() -> Weight { + (55_264_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn force_create() -> Weight { + (28_173_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn destroy(n: u32, m: u32, a: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 32_000 + .saturating_add((23_077_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 32_000 + .saturating_add((1_723_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 32_000 + .saturating_add((1_534_000 as Weight).saturating_mul(a as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(n as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(n as Weight))) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(m as Weight))) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(a as Weight))) + } + fn mint() -> Weight { + (73_250_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + fn burn() -> Weight { + (74_443_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + fn transfer() -> Weight { + (54_690_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + fn redeposit(i: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 19_000 + .saturating_add((34_624_000 as Weight).saturating_mul(i as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(i as Weight))) + } + fn freeze() -> Weight { + (39_505_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn thaw() -> Weight { + (38_844_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn freeze_class() -> Weight { + (28_739_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn thaw_class() -> Weight { + (28_963_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn transfer_ownership() -> Weight { + (65_160_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn set_team() -> Weight { + (30_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn force_asset_status() -> Weight { + (29_145_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn set_attribute() -> Weight { + (88_923_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn clear_attribute() -> Weight { + (79_878_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn set_metadata() -> Weight { + (67_110_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn clear_metadata() -> Weight { + (66_191_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn set_class_metadata() -> Weight { + (65_558_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn clear_class_metadata() -> Weight { + (60_135_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn approve_transfer() -> Weight { + (40_337_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn cancel_approval() -> Weight { + (40_770_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn create() -> Weight { + (55_264_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn force_create() -> Weight { + (28_173_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn destroy(n: u32, m: u32, a: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 32_000 + .saturating_add((23_077_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 32_000 + .saturating_add((1_723_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 32_000 + .saturating_add((1_534_000 as Weight).saturating_mul(a as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(n as Weight))) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(n as Weight))) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(m as Weight))) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(a as Weight))) + } + fn mint() -> Weight { + (73_250_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + fn burn() -> Weight { + (74_443_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + fn transfer() -> Weight { + (54_690_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + fn redeposit(i: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 19_000 + .saturating_add((34_624_000 as Weight).saturating_mul(i as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(i as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(i as Weight))) + } + fn freeze() -> Weight { + (39_505_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn thaw() -> Weight { + (38_844_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn freeze_class() -> Weight { + (28_739_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn thaw_class() -> Weight { + (28_963_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn transfer_ownership() -> Weight { + (65_160_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn set_team() -> Weight { + (30_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn force_asset_status() -> Weight { + (29_145_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn set_attribute() -> Weight { + (88_923_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn clear_attribute() -> Weight { + (79_878_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn set_metadata() -> Weight { + (67_110_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn clear_metadata() -> Weight { + (66_191_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn set_class_metadata() -> Weight { + (65_558_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn clear_class_metadata() -> Weight { + (60_135_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn approve_transfer() -> Weight { + (40_337_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn cancel_approval() -> Weight { + (40_770_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } +} diff --git a/primitives/arithmetic/src/traits.rs b/primitives/arithmetic/src/traits.rs index ea297077e351c..d0ce921d9d342 100644 --- a/primitives/arithmetic/src/traits.rs +++ b/primitives/arithmetic/src/traits.rs @@ -127,6 +127,34 @@ pub trait Saturating { /// Saturating exponentiation. Compute `self.pow(exp)`, saturating at the numeric bounds /// instead of overflowing. fn saturating_pow(self, exp: usize) -> Self; + + /// Increment self by one, saturating. + fn saturating_inc(&mut self) where Self: One { + let mut o = Self::one(); + sp_std::mem::swap(&mut o, self); + *self = o.saturating_add(One::one()); + } + + /// Decrement self by one, saturating at zero. + fn saturating_dec(&mut self) where Self: One { + let mut o = Self::one(); + sp_std::mem::swap(&mut o, self); + *self = o.saturating_sub(One::one()); + } + + /// Increment self by some `amount`, saturating. + fn saturating_accrue(&mut self, amount: Self) where Self: One { + let mut o = Self::one(); + sp_std::mem::swap(&mut o, self); + *self = o.saturating_add(amount); + } + + /// Decrement self by some `amount`, saturating at zero. + fn saturating_reduce(&mut self, amount: Self) where Self: One { + let mut o = Self::one(); + sp_std::mem::swap(&mut o, self); + *self = o.saturating_sub(amount); + } } impl Saturating for T {