From 675676ca86e51c0e3bb7b07a409ec00e8fe84358 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Fri, 5 Jul 2024 13:48:47 +0700 Subject: [PATCH 1/2] Add market pallet --- Cargo.lock | 25 ++ Cargo.toml | 1 + common/src/traits.rs | 14 + pallets/assets/src/lib.rs | 8 + pallets/market/Cargo.toml | 60 ++++ pallets/market/src/benchmarking.rs | 192 +++++++++++ pallets/market/src/lib.rs | 356 +++++++++++++++++++++ pallets/market/src/mock.rs | 140 ++++++++ pallets/market/src/tests.rs | 498 +++++++++++++++++++++++++++++ pallets/market/src/weights.rs | 214 +++++++++++++ runtime/Cargo.toml | 7 +- runtime/src/lib.rs | 15 +- 12 files changed, 1528 insertions(+), 2 deletions(-) create mode 100644 pallets/market/Cargo.toml create mode 100644 pallets/market/src/benchmarking.rs create mode 100644 pallets/market/src/lib.rs create mode 100644 pallets/market/src/mock.rs create mode 100644 pallets/market/src/tests.rs create mode 100644 pallets/market/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index cd7bafe1a0..6e8241529c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3758,6 +3758,7 @@ dependencies = [ "liquidity-proxy-benchmarking", "liquidity-proxy-runtime-api", "log", + "market", "mock-liquidity-source", "multicollateral-bonding-curve-pool", "multisig-verifier", @@ -5849,6 +5850,30 @@ dependencies = [ "libc", ] +[[package]] +name = "market" +version = "1.0.1" +dependencies = [ + "assets", + "common", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "parity-scale-codec", + "permissions", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "technical", +] + [[package]] name = "match_cfg" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3ee7b45490..a0e5569f28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,6 +280,7 @@ members = [ "pallets/band", "pallets/qa-tools", "pallets/regulated-assets", + "pallets/market", "node/", "utils/parse", "utils/generate-bags", diff --git a/common/src/traits.rs b/common/src/traits.rs index e1abf03431..dd08044f16 100644 --- a/common/src/traits.rs +++ b/common/src/traits.rs @@ -1243,6 +1243,12 @@ pub trait AssetManager< amount: Balance, ) -> DispatchResult; + fn burn_unchecked( + asset_id: &Self::AssetId, + from: &T::AccountId, + amount: Balance, + ) -> DispatchResult; + fn burn( origin: OriginFor, asset_id: Self::AssetId, @@ -1350,6 +1356,14 @@ impl DispatchResult { + unimplemented!() + } + fn burn( _origin: OriginFor, _asset_id: Self::AssetId, diff --git a/pallets/assets/src/lib.rs b/pallets/assets/src/lib.rs index 00657ad080..418a36eb4a 100644 --- a/pallets/assets/src/lib.rs +++ b/pallets/assets/src/lib.rs @@ -1126,6 +1126,14 @@ impl Self::mint_unchecked(asset_id, to, amount) } + fn burn_unchecked( + asset_id: &Self::AssetId, + from: &T::AccountId, + amount: Balance, + ) -> DispatchResult { + Self::burn_unchecked(asset_id, from, amount) + } + fn burn( origin: OriginFor, asset_id: Self::AssetId, diff --git a/pallets/market/Cargo.toml b/pallets/market/Cargo.toml new file mode 100644 index 0000000000..595cdbe7b9 --- /dev/null +++ b/pallets/market/Cargo.toml @@ -0,0 +1,60 @@ +[package] +edition = '2021' +authors = ['Polka Biome Ltd. '] +license = "BSD-4-Clause" +homepage = 'https://sora.org' +repository = 'https://github.com/sora-xor/sora2-network' +name = 'market' +version = '1.0.1' + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2", default-features = false, features = ["derive"] } +frame-support = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +frame-system = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +frame-benchmarking = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false, optional = true } +sp-core = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-runtime = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-std = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +common = { path = "../../common", default-features = false } +hex-literal = "0.4.1" +traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library.git", package = "orml-traits", default-features = false } + +[dev-dependencies] +sp-core = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-io = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-runtime = { version = "7.0.0", git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +pallet-balances = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38" } +currencies = { git = "https://github.com/open-web3-stack/open-runtime-module-library.git", package = "orml-currencies" } +tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library.git", package = "orml-tokens", default-features = false } +hex-literal = { version = "0.4.1"} +common = { path = "../../common", features = ['test']} +assets = { path = "../assets" } +permissions = { path = "../permissions" } +technical = { path = "../technical" } + +[features] +default = ['std'] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-std/std", + "sp-runtime/std", + "traits/std", + "common/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", +] + +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/market/src/benchmarking.rs b/pallets/market/src/benchmarking.rs new file mode 100644 index 0000000000..231dd9cc55 --- /dev/null +++ b/pallets/market/src/benchmarking.rs @@ -0,0 +1,192 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Market module benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::benchmarks; +use frame_system::RawOrigin; + +use common::{AssetManager, AssetName, AssetSymbol, DEFAULT_BALANCE_PRECISION}; + +fn asset_owner() -> T::AccountId { + frame_benchmarking::account("owner", 0, 0) +} + +fn buyer(i: u32) -> T::AccountId { + frame_benchmarking::account("buyer", i, 0) +} + +fn add_product(price_asset: AssetIdOf) -> AssetIdOf { + let owner = asset_owner::(); + + Pallet::::create_new_product( + owner, + common::AssetName(b"PRODUCT".to_vec()), + common::AssetSymbol(b"PRODUCT".to_vec()), + common::Description(b"PRODUCT".to_vec()), + common::ContentSource(b"PRODUCT".to_vec()), + Product { + price_asset, + price: 100, + extensions: vec![], + }, + ) + .expect("Failed to register product") +} + +fn add_asset() -> AssetIdOf { + let owner = asset_owner::(); + frame_system::Pallet::::inc_providers(&owner); + + T::AssetManager::register_from( + &owner, + AssetSymbol(b"TOKEN".to_vec()), + AssetName(b"TOKEN".to_vec()), + DEFAULT_BALANCE_PRECISION, + 10000, + true, + None, + None, + ) + .expect("Failed to register asset") +} + +benchmarks! { + create_product { + let owner = asset_owner::(); + let owner_origin: ::RuntimeOrigin = RawOrigin::Signed(owner).into(); + let price_asset = add_asset::(); + let required_product = add_product::(price_asset); + let disallowed_product = add_product::(price_asset); + }: { + Pallet::::create_product(owner_origin, + common::AssetName(b"PRODUCT".to_vec()), + common::AssetSymbol(b"PRODUCT".to_vec()), + common::Description(b"PRODUCT".to_vec()), + common::ContentSource(b"PRODUCT".to_vec()), + Product { + price_asset, + price: 100, + extensions: vec![ + Extension::Expirable(10u32.into()), + Extension::MaxAmount(10), + Extension::RequiredProduct(required_product, 1), + Extension::DisallowedProduct(disallowed_product) + ], + } + ).unwrap(); + } + + buy { + let owner = asset_owner::(); + let owner_origin: ::RuntimeOrigin = RawOrigin::Signed(owner.clone()).into(); + let price_asset = add_asset::(); + let required_product = add_product::(price_asset); + let disallowed_product = add_product::(price_asset); + let product = Pallet::::create_new_product(owner.clone(), + common::AssetName(b"PRODUCT".to_vec()), + common::AssetSymbol(b"PRODUCT".to_vec()), + common::Description(b"PRODUCT".to_vec()), + common::ContentSource(b"PRODUCT".to_vec()), + Product { + price_asset, + price: 100, + extensions: vec![ + Extension::Expirable(10u32.into()), + Extension::MaxAmount(10), + Extension::RequiredProduct(required_product, 1), + Extension::DisallowedProduct(disallowed_product) + ], + } + ).unwrap(); + Pallet::::buy(owner_origin.clone(), required_product, 1).unwrap(); + }: { + Pallet::::buy(owner_origin, product, 1).unwrap(); + } + verify { + assert_eq!( + T::AssetInfoProvider::total_balance(&product, &owner).unwrap(), + 1 + ); + } + + on_initialize { + let a in 1..50; + let owner = asset_owner::(); + let owner_origin: ::RuntimeOrigin = RawOrigin::Signed(owner.clone()).into(); + let price_asset = add_asset::(); + let product = Pallet::::create_new_product(owner.clone(), + common::AssetName(b"PRODUCT".to_vec()), + common::AssetSymbol(b"PRODUCT".to_vec()), + common::Description(b"PRODUCT".to_vec()), + common::ContentSource(b"PRODUCT".to_vec()), + Product { + price_asset, + price: 100, + extensions: vec![ + Extension::Expirable(10u32.into()), + ], + } + ).unwrap(); + for i in 0..a { + let buyer = buyer::(i); + let buyer_origin: ::RuntimeOrigin = RawOrigin::Signed(buyer.clone()).into(); + T::AssetManager::mint_unchecked(&price_asset, &buyer, 1000).unwrap(); + Pallet::::buy(buyer_origin, product, 1).unwrap(); + assert_eq!( + T::AssetInfoProvider::total_balance(&product, &buyer).unwrap(), + 1 + ); + } + frame_system::Pallet::::set_block_number(frame_system::Pallet::::block_number() + 10u32.into()); + }: { + Pallet::::on_initialize(frame_system::Pallet::::block_number()); + } + verify { + assert_eq!(Expirations::::iter().collect::>(), vec![]); + for i in 0..a { + let buyer = buyer::(i); + assert_eq!( + T::AssetInfoProvider::total_balance(&product, &buyer).unwrap(), + 0 + ); + } + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::new_test_ext(), + crate::mock::Runtime + ); +} diff --git a/pallets/market/src/lib.rs b/pallets/market/src/lib.rs new file mode 100644 index 0000000000..84ef34cb21 --- /dev/null +++ b/pallets/market/src/lib.rs @@ -0,0 +1,356 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg_attr(not(feature = "std"), no_std)] + +use common::AssetInfoProvider; +use common::AssetManager; +use common::AssetRegulator; +use common::Balance; +use common::{AccountIdOf, AssetIdOf}; +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; +use sp_runtime::traits::Zero; +use sp_runtime::Saturating; +use sp_std::collections::btree_set::BTreeSet; +use sp_std::prelude::*; +pub use weights::WeightInfo; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +mod benchmarking; + +pub mod weights; + +pub use pallet::*; + +#[derive(RuntimeDebug, Encode, Decode, TypeInfo, Clone, Copy, PartialEq, Eq)] +pub enum Extension { + MaxAmount(Balance), + RequiredProduct(AssetId, Balance), + DisallowedProduct(AssetId), + Expirable(BlockNumber), +} + +#[derive(RuntimeDebug, Encode, Decode, TypeInfo, Clone, PartialEq, Eq)] +pub struct Product { + price_asset: AssetId, + price: Balance, + extensions: Vec>, +} + +#[frame_support::pallet] +pub mod pallet { + + use super::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + common::Config { + /// Event type of this pallet. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type AssetInfoProvider: common::AssetInfoProvider< + AssetIdOf, + AccountIdOf, + common::AssetSymbol, + common::AssetName, + common::BalancePrecision, + common::ContentSource, + common::Description, + >; + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::storage] + #[pallet::getter(fn product)] + pub type Products = StorageMap< + _, + Identity, + AssetIdOf, + Product, AssetIdOf>, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn expiration)] + pub type Expirations = StorageDoubleMap< + _, + Blake2_128Concat, + BlockNumberFor, + Identity, + (T::AccountId, AssetIdOf), + Balance, + ValueQuery, + >; + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_initialize(now: T::BlockNumber) -> Weight { + let mut processed_expirations = 0; + for ((account_id, product_id), value_to_burn) in Expirations::::drain_prefix(now) { + processed_expirations += 1; + if let Err(err) = + T::AssetManager::burn_unchecked(&product_id, &account_id, value_to_burn) + { + frame_support::log::warn!( + "Unable to burn expired product, product_id: {:?}, account_id {:?}, amount: {}, err: {:?}", + product_id, + account_id, + value_to_burn, + err + ); + } + } + ::WeightInfo::on_initialize(processed_expirations) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ProductRegistered { asset_id: AssetIdOf }, + } + + #[pallet::error] + pub enum Error { + ArithmeticError, + ProductNotFound, + ProductAlreadyExist, + OperationIsNotPermittedForProduct, + MaxAmountExceeded, + MissingRequiredProduct, + HaveDisallowedProduct, + ZeroMaxAmount, + ZeroExpiration, + MultipleExtensionsNotAllowed, + AmbigiousProductRequirements, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::create_product())] + pub fn create_product( + origin: OriginFor, + name: common::AssetName, + symbol: common::AssetSymbol, + description: common::Description, + content_source: common::ContentSource, + product: Product, AssetIdOf>, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::create_new_product(who, name, symbol, description, content_source, product)?; + Ok(().into()) + } + + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::buy())] + pub fn buy( + origin: OriginFor, + product_id: AssetIdOf, + amount: Balance, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let product = Products::::get(&product_id).ok_or(Error::::ProductNotFound)?; + let asset_owner = T::AssetInfoProvider::get_asset_owner(&product_id)?; + let amount_to_pay = amount + .checked_mul(product.price) + .ok_or(Error::::ArithmeticError)?; + T::AssetManager::transfer_from( + &product.price_asset, + &who, + &asset_owner, + amount_to_pay, + )?; + for extension in product.extensions.iter() { + Self::apply_extension(extension, &who, &product_id, amount)?; + } + T::AssetManager::mint_unchecked(&product_id, &who, amount)?; + Ok(().into()) + } + } +} + +impl Pallet { + pub fn create_new_product( + who: AccountIdOf, + name: common::AssetName, + symbol: common::AssetSymbol, + description: common::Description, + content_source: common::ContentSource, + product: Product, AssetIdOf>, + ) -> Result, DispatchError> { + Self::verify_extensions(&product.extensions)?; + let asset_id = T::AssetManager::register_from( + &who, + symbol, + name, + 0, + 0, + true, + Some(content_source), + Some(description), + )?; + Products::::insert(asset_id, product); + Self::deposit_event(Event::::ProductRegistered { asset_id }); + Ok(asset_id) + } + + fn apply_extension( + extension: &Extension, AssetIdOf>, + who: &T::AccountId, + product_id: &AssetIdOf, + amount: Balance, + ) -> DispatchResult { + match extension { + Extension::MaxAmount(max_amount) => { + ensure!( + T::AssetInfoProvider::total_balance(product_id, who)?.saturating_add(amount) + <= *max_amount, + Error::::MaxAmountExceeded + ); + } + Extension::RequiredProduct(product_id, min_amount) => { + ensure!( + T::AssetInfoProvider::total_balance(product_id, who)? >= *min_amount, + Error::::MissingRequiredProduct + ); + } + Extension::DisallowedProduct(product_id) => { + ensure!( + T::AssetInfoProvider::total_balance(product_id, who)? == 0, + Error::::HaveDisallowedProduct + ); + } + Extension::Expirable(expiration) => { + let now = frame_system::Pallet::::block_number(); + Expirations::::mutate( + now.saturating_add(*expiration), + (who, product_id), + |amount_to_burn| { + *amount_to_burn = amount_to_burn.saturating_add(amount); + }, + ) + } + } + Ok(()) + } + + fn verify_extensions( + extensions: &[Extension, AssetIdOf>], + ) -> DispatchResult { + let mut has_max_amount = false; + let mut has_expiration = false; + let mut required_products = BTreeSet::new(); + let mut disallowed_products = BTreeSet::new(); + for extension in extensions { + match extension { + Extension::MaxAmount(max_amount) => { + ensure!(!has_max_amount, Error::::MultipleExtensionsNotAllowed); + has_max_amount = true; + ensure!(!max_amount.is_zero(), Error::::ZeroMaxAmount); + } + Extension::RequiredProduct(product_id, _min_amount) => { + ensure!( + !disallowed_products.contains(product_id), + Error::::AmbigiousProductRequirements + ); + ensure!( + required_products.insert(*product_id), + Error::::AmbigiousProductRequirements + ); + ensure!( + Products::::contains_key(product_id), + Error::::ProductNotFound + ); + } + Extension::DisallowedProduct(product_id) => { + ensure!( + !required_products.contains(product_id), + Error::::AmbigiousProductRequirements + ); + ensure!( + disallowed_products.insert(*product_id), + Error::::AmbigiousProductRequirements + ); + ensure!( + Products::::contains_key(product_id), + Error::::ProductNotFound + ); + } + Extension::Expirable(expiration) => { + ensure!(!has_expiration, Error::::MultipleExtensionsNotAllowed); + has_expiration = true; + ensure!(!expiration.is_zero(), Error::::ZeroExpiration); + } + } + } + Ok(()) + } +} + +impl AssetRegulator> for Pallet { + fn assign_permission( + _owner: &T::AccountId, + _asset_id: &AssetIdOf, + _permission_id: &common::permissions::PermissionId, + ) -> Result<(), DispatchError> { + Ok(()) + } + + fn check_permission( + _issuer: &T::AccountId, + _affected_account: &T::AccountId, + asset_id: &AssetIdOf, + permission_id: &common::permissions::PermissionId, + ) -> Result<(), DispatchError> { + if matches!( + *permission_id, + common::permissions::TRANSFER | common::permissions::BURN | common::permissions::MINT + ) && Products::::contains_key(asset_id) + { + frame_support::fail!(Error::::OperationIsNotPermittedForProduct); + } + Ok(()) + } +} diff --git a/pallets/market/src/mock.rs b/pallets/market/src/mock.rs new file mode 100644 index 0000000000..976c62d0a5 --- /dev/null +++ b/pallets/market/src/mock.rs @@ -0,0 +1,140 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{self as market}; +use common::mock::ExistentialDeposits; +use common::{ + mock_common_config, mock_currencies_config, mock_frame_system_config, + mock_pallet_balances_config, mock_permissions_config, mock_technical_config, + mock_tokens_config, Amount, AssetId32, DEXId, LiquiditySourceType, PredefinedAssetId, XOR, XST, +}; +use currencies::BasicCurrencyAdapter; +use frame_support::parameter_types; +use frame_support::traits::{Everything, OnInitialize}; +use hex_literal::hex; +use sp_core::H256; +use sp_runtime::testing::Header; +use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; + +use sp_runtime::traits::{IdentifyAccount, Verify}; +use sp_runtime::MultiSignature; + +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type AssetId = AssetId32; +pub type Balance = u128; +pub type Signature = MultiSignature; +pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +pub type Block = frame_system::mocking::MockBlock; +pub type TechAssetId = common::TechAssetId; +pub type TechAccountId = common::TechAccountId; +pub type BlockNumber = u64; + +mock_common_config!(Runtime); +mock_currencies_config!(Runtime); +mock_tokens_config!(Runtime); +mock_pallet_balances_config!(Runtime); +mock_frame_system_config!(Runtime); +mock_permissions_config!(Runtime); +mock_technical_config!(Runtime); + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Assets: assets, + Balances: pallet_balances, + Tokens: tokens, + Permissions: permissions, + Technical: technical, + Market: market, + } +); + +parameter_types! { + pub const GetBaseAssetId: AssetId = XOR; + pub const GetBuyBackAssetId: AssetId = XST; + pub GetBuyBackSupplyAssets: Vec = vec![]; + pub const GetBuyBackPercentage: u8 = 0; + pub const GetBuyBackAccountId: AccountId = AccountId::new(hex!( + "0000000000000000000000000000000000000000000000000000000000000023" + )); + pub const GetBuyBackDexId: DEXId = DEXId::Polkaswap; +} + +impl assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ExtraAccountId = [u8; 32]; + type ExtraAssetRecordArg = + common::AssetIdExtraAssetRecordArg; + type AssetId = AssetId; + type GetBaseAssetId = GetBaseAssetId; + type GetBuyBackAssetId = GetBuyBackAssetId; + type GetBuyBackSupplyAssets = GetBuyBackSupplyAssets; + type GetBuyBackPercentage = GetBuyBackPercentage; + type GetBuyBackAccountId = GetBuyBackAccountId; + type GetBuyBackDexId = GetBuyBackDexId; + type BuyBackLiquidityProxy = (); + type Currency = currencies::Pallet; + type GetTotalBalance = (); + type WeightInfo = (); + type AssetRegulator = market::Pallet; +} + +impl crate::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AssetInfoProvider = assets::Pallet; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into(); + ext.execute_with(|| { + System::set_block_number(1); // No events in zero block + System::inc_providers(&common::mock::alice()); + System::inc_providers(&common::mock::bob()); + }); + ext +} + +pub fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + let block_number = System::block_number() + 1; + System::set_block_number(block_number); + Market::on_initialize(block_number); + } +} diff --git a/pallets/market/src/tests.rs b/pallets/market/src/tests.rs new file mode 100644 index 0000000000..4500a9fbbd --- /dev/null +++ b/pallets/market/src/tests.rs @@ -0,0 +1,498 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::mock::*; +use crate::*; +use common::mock::{alice, bob}; +use common::{balance, AssetName, AssetSymbol, ContentSource, Description}; +use frame_support::assert_err; +use frame_support::assert_ok; + +type Market = Pallet; + +fn test_price_asset() -> AssetId { + let asset_id = Assets::register_from( + &alice(), + AssetSymbol(b"USD".to_vec()), + AssetName(b"USD".to_vec()), + 18, + balance!(1000), + true, + None, + None, + ) + .unwrap(); + Assets::mint_unchecked(&asset_id, &bob(), balance!(1000)).unwrap(); + asset_id +} + +#[test] +fn test_create_product() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + assert_ok!(Market::create_product( + RuntimeOrigin::signed(alice()), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![] + } + )); + let (product_id, product) = Products::::iter().next().unwrap(); + System::assert_has_event( + Event::::ProductRegistered { + asset_id: product_id, + } + .into(), + ); + assert_eq!( + product, + Product { + price_asset, + price: balance!(1), + extensions: vec![] + } + ); + }) +} + +#[test] +fn test_buy_product() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + let product_id = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![], + }, + ) + .unwrap(); + + assert_ok!(Market::buy(RuntimeOrigin::signed(bob()), product_id, 1)); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 1); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(999) + ); + + assert_ok!(Market::buy(RuntimeOrigin::signed(bob()), product_id, 7)); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 8); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(992) + ); + }) +} + +#[test] +fn test_buy_expirable_product() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + let product_id = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![Extension::Expirable(100)], + }, + ) + .unwrap(); + + assert_ok!(Market::buy(RuntimeOrigin::signed(bob()), product_id, 1)); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 1); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(999) + ); + assert_eq!(Expirations::::get(101, (bob(), product_id)), 1); + + run_to_block(50); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 1); + assert_ok!(Market::buy(RuntimeOrigin::signed(bob()), product_id, 1)); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 2); + assert_eq!(Expirations::::get(101, (bob(), product_id)), 1); + assert_eq!(Expirations::::get(150, (bob(), product_id)), 1); + + run_to_block(101); + assert_eq!(Expirations::::get(150, (bob(), product_id)), 1); + assert!(!Expirations::::contains_key( + 101, + (bob(), product_id) + )); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 1); + + run_to_block(150); + assert!(!Expirations::::contains_key( + 150, + (bob(), product_id) + )); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 0); + }) +} + +#[test] +fn test_buy_product_with_requirement() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + let checkin_product_id = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![], + }, + ) + .unwrap(); + + let upgrade_product_id = Market::create_new_product( + alice(), + AssetName(b"Upgrade".to_vec()), + AssetSymbol(b"UPGRADE".to_vec()), + Description(b"User Upgrade".to_vec()), + ContentSource(b"https://upgrade".to_vec()), + Product { + price_asset, + price: balance!(10), + extensions: vec![Extension::RequiredProduct(checkin_product_id, 2)], + }, + ) + .unwrap(); + + assert_err!( + Market::buy(RuntimeOrigin::signed(bob()), upgrade_product_id, 1), + Error::::MissingRequiredProduct + ); + assert_ok!(Market::buy( + RuntimeOrigin::signed(bob()), + checkin_product_id, + 1 + )); + assert_eq!( + Assets::total_balance(&checkin_product_id, &bob()).unwrap(), + 1 + ); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(999) + ); + assert_err!( + Market::buy(RuntimeOrigin::signed(bob()), upgrade_product_id, 1), + Error::::MissingRequiredProduct + ); + assert_ok!(Market::buy( + RuntimeOrigin::signed(bob()), + checkin_product_id, + 1 + )); + assert_eq!( + Assets::total_balance(&checkin_product_id, &bob()).unwrap(), + 2 + ); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(998) + ); + assert_ok!(Market::buy( + RuntimeOrigin::signed(bob()), + upgrade_product_id, + 1 + )); + assert_eq!( + Assets::total_balance(&upgrade_product_id, &bob()).unwrap(), + 1 + ); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(988) + ); + }) +} + +#[test] +fn test_buy_max_amount_product() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + let product_id = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![Extension::MaxAmount(2)], + }, + ) + .unwrap(); + + assert_err!( + Market::buy(RuntimeOrigin::signed(bob()), product_id, 3), + Error::::MaxAmountExceeded + ); + + assert_ok!(Market::buy(RuntimeOrigin::signed(bob()), product_id, 1)); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 1); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(999) + ); + + assert_err!( + Market::buy(RuntimeOrigin::signed(bob()), product_id, 2), + Error::::MaxAmountExceeded + ); + + assert_ok!(Market::buy(RuntimeOrigin::signed(bob()), product_id, 1)); + assert_eq!(Assets::total_balance(&product_id, &bob()).unwrap(), 2); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(998) + ); + + assert_err!( + Market::buy(RuntimeOrigin::signed(bob()), product_id, 1), + Error::::MaxAmountExceeded + ); + }) +} + +#[test] +fn test_buy_product_with_disallowed_product() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + let checkin_product_id = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![], + }, + ) + .unwrap(); + + let upgrade_product_id = Market::create_new_product( + alice(), + AssetName(b"Upgrade".to_vec()), + AssetSymbol(b"UPGRADE".to_vec()), + Description(b"User Upgrade".to_vec()), + ContentSource(b"https://upgrade".to_vec()), + Product { + price_asset, + price: balance!(10), + extensions: vec![Extension::DisallowedProduct(checkin_product_id)], + }, + ) + .unwrap(); + + assert_ok!(Market::buy( + RuntimeOrigin::signed(bob()), + upgrade_product_id, + 1 + ),); + assert_eq!( + Assets::total_balance(&upgrade_product_id, &bob()).unwrap(), + 1 + ); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(990) + ); + assert_ok!(Market::buy( + RuntimeOrigin::signed(bob()), + checkin_product_id, + 1 + )); + assert_eq!( + Assets::total_balance(&checkin_product_id, &bob()).unwrap(), + 1 + ); + assert_eq!( + Assets::total_balance(&price_asset, &bob()).unwrap(), + balance!(989) + ); + assert_err!( + Market::buy(RuntimeOrigin::signed(bob()), upgrade_product_id, 1), + Error::::HaveDisallowedProduct + ); + }) +} + +#[test] +fn test_extensions_verification() { + new_test_ext().execute_with(|| { + let price_asset = test_price_asset(); + let product_id_2 = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![Extension::MaxAmount(2)], + }, + ) + .unwrap(); + let product_id_1 = Market::create_new_product( + alice(), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions: vec![Extension::MaxAmount(2)], + }, + ) + .unwrap(); + for (extensions, err) in [ + (vec![], None), + (vec![Extension::MaxAmount(1)], None), + ( + vec![Extension::MaxAmount(0)], + Some(Error::::ZeroMaxAmount), + ), + ( + vec![Extension::MaxAmount(1), Extension::MaxAmount(2)], + Some(Error::::MultipleExtensionsNotAllowed), + ), + (vec![Extension::Expirable(1)], None), + ( + vec![Extension::Expirable(0)], + Some(Error::::ZeroExpiration), + ), + ( + vec![Extension::Expirable(1), Extension::Expirable(2)], + Some(Error::::MultipleExtensionsNotAllowed), + ), + (vec![Extension::Expirable(1), Extension::MaxAmount(1)], None), + ( + vec![ + Extension::MaxAmount(1), + Extension::Expirable(1), + Extension::Expirable(2), + ], + Some(Error::::MultipleExtensionsNotAllowed), + ), + (vec![Extension::RequiredProduct(product_id_1, 1)], None), + ( + vec![ + Extension::RequiredProduct(product_id_1, 1), + Extension::DisallowedProduct(product_id_2), + ], + None, + ), + ( + vec![ + Extension::RequiredProduct(product_id_1, 1), + Extension::RequiredProduct(product_id_1, 1), + ], + Some(Error::AmbigiousProductRequirements), + ), + ( + vec![ + Extension::RequiredProduct(product_id_1, 1), + Extension::DisallowedProduct(product_id_1), + ], + Some(Error::AmbigiousProductRequirements), + ), + ( + vec![ + Extension::DisallowedProduct(product_id_1), + Extension::RequiredProduct(product_id_1, 1), + ], + Some(Error::AmbigiousProductRequirements), + ), + ( + vec![ + Extension::DisallowedProduct(product_id_1), + Extension::DisallowedProduct(product_id_1), + ], + Some(Error::AmbigiousProductRequirements), + ), + ] { + if let Some(err) = err { + assert_err!( + Market::create_product( + RuntimeOrigin::signed(alice()), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions + } + ), + err + ); + } else { + assert_ok!(Market::create_product( + RuntimeOrigin::signed(alice()), + AssetName(b"CheckIn".to_vec()), + AssetSymbol(b"CHECKIN".to_vec()), + Description(b"User Check-in".to_vec()), + ContentSource(b"https://checkin".to_vec()), + Product { + price_asset, + price: balance!(1), + extensions + } + )); + } + } + }) +} diff --git a/pallets/market/src/weights.rs b/pallets/market/src/weights.rs new file mode 100644 index 0000000000..bfb2e81ad4 --- /dev/null +++ b/pallets/market/src/weights.rs @@ -0,0 +1,214 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Autogenerated weights for market +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2024-07-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `X670`, CPU: `AMD Ryzen 9 7950X 16-Core Processor` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("local"), DB CACHE: 1024 + +// Executed Command: +// target/release/framenode +// benchmark +// pallet +// --chain=local +// --execution=wasm +// --wasm-execution=compiled +// --pallet +// market +// --extrinsic +// * +// --steps +// 50 +// --repeat +// 20 +// --output +// ./ +// --header=./misc/file_header.txt +// --template=./misc/pallet-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for market. +pub trait WeightInfo { + fn create_product() -> Weight; + fn buy() -> Weight; + fn on_initialize(a: u32, ) -> Weight; +} + +/// Weights for market using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Market Products (r:2 w:1) + /// Proof Skipped: Market Products (max_values: None, max_size: None, mode: Measured) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets AssetOwners (r:1 w:1) + /// Proof Skipped: Assets AssetOwners (max_values: None, max_size: None, mode: Measured) + /// Storage: Permissions Owners (r:2 w:2) + /// Proof Skipped: Permissions Owners (max_values: None, max_size: None, mode: Measured) + /// Storage: Permissions Permissions (r:5 w:1) + /// Proof Skipped: Permissions Permissions (max_values: None, max_size: None, mode: Measured) + /// Storage: Assets AssetInfos (r:0 w:1) + /// Proof Skipped: Assets AssetInfos (max_values: None, max_size: None, mode: Measured) + fn create_product() -> Weight { + // Proof Size summary in bytes: + // Measured: `2764` + // Estimated: `41173` + // Minimum execution time: 86_821_000 picoseconds. + Weight::from_parts(87_501_000, 41173) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: Market Products (r:2 w:0) + /// Proof Skipped: Market Products (max_values: None, max_size: None, mode: Measured) + /// Storage: Assets AssetOwners (r:2 w:0) + /// Proof Skipped: Assets AssetOwners (max_values: None, max_size: None, mode: Measured) + /// Storage: RegulatedAssets SoulboundAsset (r:1 w:0) + /// Proof: RegulatedAssets SoulboundAsset (max_values: None, max_size: Some(320580), added: 323055, mode: MaxEncodedLen) + /// Storage: RegulatedAssets RegulatedAsset (r:1 w:0) + /// Proof: RegulatedAssets RegulatedAsset (max_values: None, max_size: Some(33), added: 2508, mode: MaxEncodedLen) + /// Storage: Market Expirations (r:1 w:1) + /// Proof Skipped: Market Expirations (max_values: None, max_size: None, mode: Measured) + /// Storage: Tokens Accounts (r:3 w:1) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(136), added: 2611, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:1 w:1) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn buy() -> Weight { + // Proof Size summary in bytes: + // Measured: `2403` + // Estimated: `358114` + // Minimum execution time: 51_000_000 picoseconds. + Weight::from_parts(51_890_000, 358114) + .saturating_add(T::DbWeight::get().reads(12_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: Market Expirations (r:51 w:50) + /// Proof Skipped: Market Expirations (max_values: None, max_size: None, mode: Measured) + /// Storage: Tokens Accounts (r:50 w:50) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(136), added: 2611, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:1 w:1) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + /// The range of component `a` is `[1, 50]`. + fn on_initialize(a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `852 + a * (303 ±0)` + // Estimated: `5854 + a * (5390 ±0)` + // Minimum execution time: 26_060_000 picoseconds. + Weight::from_parts(10_062_137, 5854) + // Standard Error: 7_555 + .saturating_add(Weight::from_parts(15_432_547, 0).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 5390).saturating_mul(a.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Market Products (r:2 w:1) + /// Proof Skipped: Market Products (max_values: None, max_size: None, mode: Measured) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets AssetOwners (r:1 w:1) + /// Proof Skipped: Assets AssetOwners (max_values: None, max_size: None, mode: Measured) + /// Storage: Permissions Owners (r:2 w:2) + /// Proof Skipped: Permissions Owners (max_values: None, max_size: None, mode: Measured) + /// Storage: Permissions Permissions (r:5 w:1) + /// Proof Skipped: Permissions Permissions (max_values: None, max_size: None, mode: Measured) + /// Storage: Assets AssetInfos (r:0 w:1) + /// Proof Skipped: Assets AssetInfos (max_values: None, max_size: None, mode: Measured) + fn create_product() -> Weight { + // Proof Size summary in bytes: + // Measured: `2764` + // Estimated: `41173` + // Minimum execution time: 86_821_000 picoseconds. + Weight::from_parts(87_501_000, 41173) + .saturating_add(RocksDbWeight::get().reads(11_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: Market Products (r:2 w:0) + /// Proof Skipped: Market Products (max_values: None, max_size: None, mode: Measured) + /// Storage: Assets AssetOwners (r:2 w:0) + /// Proof Skipped: Assets AssetOwners (max_values: None, max_size: None, mode: Measured) + /// Storage: RegulatedAssets SoulboundAsset (r:1 w:0) + /// Proof: RegulatedAssets SoulboundAsset (max_values: None, max_size: Some(320580), added: 323055, mode: MaxEncodedLen) + /// Storage: RegulatedAssets RegulatedAsset (r:1 w:0) + /// Proof: RegulatedAssets RegulatedAsset (max_values: None, max_size: Some(33), added: 2508, mode: MaxEncodedLen) + /// Storage: Market Expirations (r:1 w:1) + /// Proof Skipped: Market Expirations (max_values: None, max_size: None, mode: Measured) + /// Storage: Tokens Accounts (r:3 w:1) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(136), added: 2611, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:1 w:1) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn buy() -> Weight { + // Proof Size summary in bytes: + // Measured: `2403` + // Estimated: `358114` + // Minimum execution time: 51_000_000 picoseconds. + Weight::from_parts(51_890_000, 358114) + .saturating_add(RocksDbWeight::get().reads(12_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: Market Expirations (r:51 w:50) + /// Proof Skipped: Market Expirations (max_values: None, max_size: None, mode: Measured) + /// Storage: Tokens Accounts (r:50 w:50) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(136), added: 2611, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:1 w:1) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + /// The range of component `a` is `[1, 50]`. + fn on_initialize(a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `852 + a * (303 ±0)` + // Estimated: `5854 + a * (5390 ±0)` + // Minimum execution time: 26_060_000 picoseconds. + Weight::from_parts(10_062_137, 5854) + // Standard Error: 7_555 + .saturating_add(Weight::from_parts(15_432_547, 0).saturating_mul(a.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(a.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 5390).saturating_mul(a.into())) + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d68e5f3d6a..531c4251cf 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -103,6 +103,7 @@ vested-rewards-runtime-api = { path = "../pallets/vested-rewards/runtime-api", d xor-fee = { path = "../pallets/xor-fee", default-features = false } xst = { path = "../pallets/xst", default-features = false } xst-benchmarking = { path = "../pallets/xst/benchmarking", default-features = false, optional = true } +market = { path = "../pallets/market", default-features = false, optional = true } # Substrate dependencies frame-benchmarking = { git = "https://github.com/sora-xor/substrate.git", branch = "polkadot-v0.9.38", default-features = false, optional = true } @@ -290,6 +291,7 @@ std = [ "xor-fee/std", "xst/std", "xst-benchmarking/std", + "market/std" ] private-net = [ @@ -313,6 +315,7 @@ wip = [ "pallet-beefy", "pallet-beefy-mmr", "regulated-assets/wip", + "market", ] ready-to-test = ["framenode-chain-spec/ready-to-test", "apollo-platform"] @@ -372,7 +375,8 @@ runtime-benchmarks = [ "beefy-light-client/runtime-benchmarks", "multisig-verifier/runtime-benchmarks", "bridge-data-signer/runtime-benchmarks", - "regulated-assets/runtime-benchmarks" + "regulated-assets/runtime-benchmarks", + "market/runtime-benchmarks", ] reduced-pswap-reward-periods = [] @@ -460,6 +464,7 @@ try-runtime = [ "multisig-verifier/try-runtime", "qa-tools/try-runtime", "regulated-assets/try-runtime", + "market/try-runtime", ] test = ["framenode-chain-spec/test", "liquidity-proxy/test", "order-book/test"] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b396cf2340..dfdc83b2c7 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -966,7 +966,7 @@ impl assets::Config for Runtime { #[cfg(feature = "wip")] // DEFI-R type AssetRegulator = ( permissions::Pallet, - regulated_assets::Pallet, + (regulated_assets::Pallet, market::Pallet), ); } @@ -2422,6 +2422,13 @@ impl regulated_assets::Config for Runtime { type WeightInfo = regulated_assets::weights::SubstrateWeight; } +#[cfg(feature = "wip")] // Market +impl market::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = market::weights::SubstrateWeight; + type AssetInfoProvider = Assets; +} + construct_runtime! { pub enum Runtime where Block = Block, @@ -2548,6 +2555,8 @@ construct_runtime! { ApolloPlatform: apollo_platform::{Pallet, Call, Storage, Event, ValidateUnsigned} = 114, #[cfg(feature = "wip")] // DEFI-R RegulatedAssets: regulated_assets::{Pallet, Call, Storage, Event} = 115, + #[cfg(feature = "wip")] // Market + Market: market::{Pallet, Call, Storage, Event} = 116, } } @@ -3312,6 +3321,8 @@ impl_runtime_apis! { list_benchmark!(list, extra, multisig_verifier, MultisigVerifier); #[cfg(feature = "wip")] // DEFI-R list_benchmark!(list, extra, regulated_assets, RegulatedAssets); + #[cfg(feature = "wip")] // Market + list_benchmark!(list, extra, market, Market); let storage_info = AllPalletsWithSystem::storage_info(); @@ -3409,6 +3420,8 @@ impl_runtime_apis! { add_benchmark!(params, batches, multisig_verifier, MultisigVerifier); #[cfg(feature = "wip")] // DEFI-R add_benchmark!(params, batches, regulated_assets, RegulatedAssets); + #[cfg(feature = "wip")] // Market + add_benchmark!(params, batches, market, Market); if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } Ok(batches) From 07be488dce6eda4d619660e2921842c42c397b3a Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Fri, 5 Jul 2024 14:02:42 +0700 Subject: [PATCH 2/2] Fix after merge --- common/src/primitives.rs | 1 + pallets/market/src/benchmarking.rs | 1 + pallets/market/src/lib.rs | 2 ++ pallets/market/src/tests.rs | 1 + 4 files changed, 5 insertions(+) diff --git a/common/src/primitives.rs b/common/src/primitives.rs index 92613a22fd..cdcae48feb 100644 --- a/common/src/primitives.rs +++ b/common/src/primitives.rs @@ -1311,6 +1311,7 @@ pub enum AssetType { NFT, Soulbound, Regulated, + Product, } /// Presents information about an asset. diff --git a/pallets/market/src/benchmarking.rs b/pallets/market/src/benchmarking.rs index 231dd9cc55..7e2de255c7 100644 --- a/pallets/market/src/benchmarking.rs +++ b/pallets/market/src/benchmarking.rs @@ -76,6 +76,7 @@ fn add_asset() -> AssetIdOf { DEFAULT_BALANCE_PRECISION, 10000, true, + AssetType::Regular, None, None, ) diff --git a/pallets/market/src/lib.rs b/pallets/market/src/lib.rs index 84ef34cb21..f31eaaf9d5 100644 --- a/pallets/market/src/lib.rs +++ b/pallets/market/src/lib.rs @@ -33,6 +33,7 @@ use common::AssetInfoProvider; use common::AssetManager; use common::AssetRegulator; +use common::AssetType; use common::Balance; use common::{AccountIdOf, AssetIdOf}; use frame_support::pallet_prelude::*; @@ -228,6 +229,7 @@ impl Pallet { 0, 0, true, + AssetType::Product, Some(content_source), Some(description), )?; diff --git a/pallets/market/src/tests.rs b/pallets/market/src/tests.rs index 4500a9fbbd..80cee44350 100644 --- a/pallets/market/src/tests.rs +++ b/pallets/market/src/tests.rs @@ -45,6 +45,7 @@ fn test_price_asset() -> AssetId { 18, balance!(1000), true, + AssetType::Regular, None, None, )