diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6b02eb6..f23b7eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: target: wasm32-unknown-unknown - name: Unit tests - run: cargo test -p bitgreen-parachain --features runtime-benchmarks + run: cargo test -p bitgreen-parachain build-docker-image: # The type of runner that the job will run on diff --git a/Cargo.lock b/Cargo.lock index 2c963886..91efd2ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,7 @@ dependencies = [ "pallet-contracts-primitives", "pallet-dex", "pallet-identity", + "pallet-kyc", "pallet-membership", "pallet-multisig", "pallet-parachain-staking", @@ -752,6 +753,7 @@ dependencies = [ "pallet-contracts-primitives", "pallet-dex", "pallet-identity", + "pallet-kyc", "pallet-membership", "pallet-multisig", "pallet-parachain-staking", @@ -5526,6 +5528,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "orml-tokens", "orml-traits", "pallet-assets", @@ -5703,6 +5706,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-kyc" +version = "0.0.1" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-membership" version = "4.0.0-dev" @@ -6387,9 +6407,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366e44391a8af4cfd6002ef6ba072bae071a96aafca98d7d448a34c5dca38b6a" +checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" dependencies = [ "arrayvec 0.7.2", "bitvec", @@ -6402,9 +6422,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9299338969a3d2f491d65f140b00ddec470858402f888af98e8642fb5e8965cd" +checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/pallets/carbon-credits-pool/src/mock.rs b/pallets/carbon-credits-pool/src/mock.rs index 04cc53e7..7b2f29af 100644 --- a/pallets/carbon-credits-pool/src/mock.rs +++ b/pallets/carbon-credits-pool/src/mock.rs @@ -142,9 +142,11 @@ impl pallet_carbon_credits::Config for Test { type ForceOrigin = frame_system::EnsureRoot; type ItemId = u32; type ProjectId = u32; + type MaxCoordinatesLength = ConstU32<8>; type GroupId = u32; type KYCProvider = KYCMembership; type MarketplaceEscrow = MarketplaceEscrowAccount; + type MaxCoordinatesLength = ConstU32<8>; type MaxAuthorizedAccountCount = ConstU32<2>; type MaxDocumentCount = ConstU32<2>; type MaxGroupSize = MaxGroupSize; diff --git a/pallets/carbon-credits/src/functions.rs b/pallets/carbon-credits/src/functions.rs index 95802d3b..0d4ab728 100644 --- a/pallets/carbon-credits/src/functions.rs +++ b/pallets/carbon-credits/src/functions.rs @@ -20,9 +20,9 @@ use sp_runtime::traits::{AccountIdConversion, CheckedAdd, CheckedSub, One, Zero} use sp_std::{cmp, convert::TryInto, vec::Vec}; use crate::{ - AssetIdLookup, AuthorizedAccounts, BatchRetireDataList, BatchRetireDataOf, Config, Error, - Event, NextAssetId, NextItemId, NextProjectId, Pallet, ProjectCreateParams, ProjectDetail, - Projects, RetiredCarbonCreditsData, RetiredCredits, + AssetIdLookup, AuthorizedAccounts, BatchGroupOf, BatchRetireDataList, BatchRetireDataOf, + Config, Error, Event, NextAssetId, NextItemId, NextProjectId, Pallet, ProjectCreateParams, + ProjectDetail, Projects, RetiredCarbonCreditsData, RetiredCredits, }; impl Pallet { @@ -291,6 +291,117 @@ impl Pallet { }) } + /// Update a project that has already been approved, this function only allows the owner to + /// update certain fields of the project description, once approved the project cannot modify + /// the batch groups data. + pub fn update_project( + admin: T::AccountId, + project_id: T::ProjectId, + params: ProjectCreateParams, + ) -> DispatchResult { + let now = frame_system::Pallet::::block_number(); + + Projects::::try_mutate(project_id, |project| -> DispatchResult { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + + // non approved project needs to be resubmitted + ensure!(project.approved, Error::::CannotUpdateUnapprovedProject); + + // only originator can resubmit + ensure!(project.originator == admin, Error::::NotAuthorised); + + let new_project = ProjectDetail { + originator: admin, + name: params.name, + description: params.description, + location: params.location, + images: params.images, + videos: params.videos, + documents: params.documents, + registry_details: params.registry_details, + sdg_details: params.sdg_details, + royalties: params.royalties, + // we don't allow editing of the project batch data + batch_groups: project.batch_groups.clone(), + created: project.created, + updated: Some(now), + approved: project.approved, + }; + + *project = new_project; + + // emit event + Self::deposit_event(Event::ProjectUpdated { project_id }); + + Ok(()) + }) + } + + /// Add a new batch group to the project, this can only be done by the originator + pub fn do_add_batch_group( + admin: T::AccountId, + project_id: T::ProjectId, + mut batch_group: BatchGroupOf, + ) -> DispatchResult { + Projects::::try_mutate(project_id, |project| -> DispatchResult { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + + // non approved project needs to be resubmitted + ensure!(project.approved, Error::::CannotUpdateUnapprovedProject); + + // only originator can resubmit + ensure!(project.originator == admin, Error::::NotAuthorised); + + let mut batch_group_map = project.batch_groups.clone(); + + let group_id: u32 = batch_group_map.len() as u32; + + let mut group_total_supply: T::Balance = Zero::zero(); + + for batch in batch_group.batches.iter() { + ensure!( + batch.total_supply > Zero::zero(), + Error::::CannotCreateProjectWithoutCredits + ); + + ensure!( + batch.minted == Zero::zero(), + Error::::CannotCreateProjectWithoutCredits + ); + + ensure!( + batch.retired == Zero::zero(), + Error::::CannotCreateProjectWithoutCredits + ); + + group_total_supply = group_total_supply + .checked_add(&batch.total_supply) + .ok_or(Error::::Overflow)?; + } + + ensure!( + group_total_supply > Zero::zero(), + Error::::CannotCreateProjectWithoutCredits + ); + + // sort batch data in ascending order of issuance year + batch_group.batches.sort_by(|x, y| x.issuance_year.cmp(&y.issuance_year)); + batch_group.total_supply = group_total_supply; + + // insert the group to BTreeMap + batch_group_map + .try_insert(group_id.into(), batch_group.clone()) + .map_err(|_| Error::::TooManyGroups)?; + + project.batch_groups = batch_group_map; + + // emit event + Self::deposit_event(Event::BatchGroupAdded { project_id, group_id: group_id.into() }); + + Ok(()) + }) + } + pub fn mint_carbon_credits( _sender: T::AccountId, project_id: T::ProjectId, diff --git a/pallets/carbon-credits/src/lib.rs b/pallets/carbon-credits/src/lib.rs index 0c643789..21b6bba7 100644 --- a/pallets/carbon-credits/src/lib.rs +++ b/pallets/carbon-credits/src/lib.rs @@ -301,6 +301,18 @@ pub mod pallet { /// Details of the retired token retire_data: BatchRetireDataList, }, + /// A project details has been updated + ProjectUpdated { + /// The ProjectId of the updated project + project_id: T::ProjectId, + }, + /// A new batch group was added to the project + BatchGroupAdded { + /// The ProjectId of the updated project + project_id: T::ProjectId, + /// GroupId of the new batch group + group_id: T::GroupId, + }, } // Errors inform users that something went wrong. @@ -340,6 +352,8 @@ pub mod pallet { TooManyGroups, /// the group does not exist GroupNotFound, + /// Can only update an approved project, use resubmit for rejected projects + CannotUpdateUnapprovedProject, } #[pallet::call] @@ -572,6 +586,34 @@ pub mod pallet { Projects::::take(project_id); Ok(()) } + + /// Modify the details of an approved project + /// Can only be called by the ProjectOwner + #[transactional] + #[pallet::weight(T::WeightInfo::create())] + pub fn update_project_details( + origin: OriginFor, + project_id: T::ProjectId, + params: ProjectCreateParams, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_kyc_approval(&sender)?; + Self::update_project(sender, project_id, params) + } + + /// Add a new batch group to the project + /// Can only be called by the ProjectOwner + #[transactional] + #[pallet::weight(T::WeightInfo::create())] + pub fn add_batch_group( + origin: OriginFor, + project_id: T::ProjectId, + batch_group: BatchGroupOf, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_kyc_approval(&sender)?; + Self::do_add_batch_group(sender, project_id, batch_group) + } } } diff --git a/pallets/carbon-credits/src/tests.rs b/pallets/carbon-credits/src/tests.rs index dea8ba14..8b481e38 100644 --- a/pallets/carbon-credits/src/tests.rs +++ b/pallets/carbon-credits/src/tests.rs @@ -1427,3 +1427,172 @@ fn force_approve_and_mint_credits_works() { assert!(stored_data.approved); }); } + +#[test] +fn update_works() { + new_test_ext().execute_with(|| { + let originator_account = 1; + let authorised_account = 10; + let project_id = 0; + + let mut creation_params = get_default_creation_params::(); + // replace the default with mutiple batches + creation_params.batch_groups = get_multiple_batch_group::(); + + assert_ok!(CarbonCredits::create( + RawOrigin::Signed(originator_account).into(), + creation_params.clone() + )); + + // unapproved project cannot be updated + assert_noop!( + CarbonCredits::update_project_details( + RawOrigin::Signed(originator_account).into(), + project_id, + creation_params.clone() + ), + Error::::CannotUpdateUnapprovedProject + ); + + // authorise the account + assert_ok!(CarbonCredits::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account + )); + assert_ok!(CarbonCredits::approve_project( + RawOrigin::Signed(authorised_account).into(), + project_id, + true + ),); + + // only originator can update + assert_noop!( + CarbonCredits::update_project_details( + RawOrigin::Signed(10).into(), + project_id, + creation_params.clone() + ), + Error::::NotAuthorised + ); + + creation_params.name = "Newname".as_bytes().to_vec().try_into().unwrap(); + // update the minted count + creation_params.batch_groups = vec![BatchGroupOf:: { + name: "batch_group_name".as_bytes().to_vec().try_into().unwrap(), + uuid: "batch_group_uuid".as_bytes().to_vec().try_into().unwrap(), + asset_id: 0_u32, + total_supply: 100_u32.into(), + minted: 10_000_u32.into(), + retired: 0_u32.into(), + batches: get_multiple_batch_list::(), + }] + .try_into() + .unwrap(); + + assert_ok!(CarbonCredits::update_project_details( + RawOrigin::Signed(originator_account).into(), + project_id, + creation_params.clone() + )); + + // ensure the storage is populated correctly + let stored_data = Projects::::get(project_id).unwrap(); + assert_eq!(stored_data.originator, originator_account); + assert_eq!(stored_data.name, creation_params.name); + assert_eq!(stored_data.registry_details, get_default_registry_details::()); + assert!(stored_data.approved); + + // the batch group should not be updated + let group_data = stored_data.batch_groups.get(&0u32).unwrap(); + assert_eq!(group_data.batches, get_multiple_batch_list::()); + assert_eq!(group_data.total_supply, 200_u32.into()); + // the minted amount should not be updated + assert_eq!(group_data.minted, 0_u32.into()); + assert_eq!(group_data.retired, 0_u32.into()); + + assert_eq!(last_event(), CarbonCreditsEvent::ProjectUpdated { project_id }.into()); + }); +} + +#[test] +fn add_batch_group_works() { + new_test_ext().execute_with(|| { + let originator_account = 1; + let authorised_account = 10; + let project_id = 0; + + let mut creation_params = get_default_creation_params::(); + // replace the default with mutiple batches + creation_params.batch_groups = get_multiple_batch_group::(); + + assert_ok!(CarbonCredits::create( + RawOrigin::Signed(originator_account).into(), + creation_params.clone() + )); + + // ensure the storage is populated correctly + let stored_data = Projects::::get(project_id).unwrap(); + assert_eq!(stored_data.originator, originator_account); + assert_eq!(stored_data.name, creation_params.name); + assert_eq!(stored_data.registry_details, get_default_registry_details::()); + assert!(!stored_data.approved); + + let group_data = stored_data.batch_groups.get(&0u32).unwrap(); + assert_eq!(stored_data.sdg_details, get_default_sdg_details::()); + assert_eq!(group_data.batches, get_multiple_batch_list::()); + assert_eq!(group_data.total_supply, 200_u32.into()); + assert_eq!(group_data.minted, 0_u32.into()); + assert_eq!(group_data.retired, 0_u32.into()); + + assert_eq!(last_event(), CarbonCreditsEvent::ProjectCreated { project_id }.into()); + + // authorise the account + assert_ok!(CarbonCredits::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account + )); + assert_ok!(CarbonCredits::approve_project( + RawOrigin::Signed(authorised_account).into(), + project_id, + true + ),); + + // add a new batch group to the project + let new_batch = BatchGroupOf:: { + name: "new_batch_group_name".as_bytes().to_vec().try_into().unwrap(), + uuid: "new_batch_group_uuid".as_bytes().to_vec().try_into().unwrap(), + asset_id: 0_u32, + total_supply: 200_u32.into(), + minted: 0_u32.into(), + retired: 0_u32.into(), + batches: get_multiple_batch_list::(), + }; + + assert_ok!(CarbonCredits::add_batch_group( + RawOrigin::Signed(originator_account).into(), + project_id, + new_batch.clone() + )); + + // ensure the storage is populated correctly + let stored_data = Projects::::get(project_id).unwrap(); + assert_eq!(stored_data.originator, originator_account); + assert_eq!(stored_data.name, creation_params.name); + assert_eq!(stored_data.registry_details, get_default_registry_details::()); + assert_eq!(stored_data.batch_groups.len(), 2); + + let group_data = stored_data.batch_groups.get(&0u32).unwrap(); + assert_eq!(group_data.batches, get_multiple_batch_list::()); + assert_eq!(group_data.total_supply, 200_u32.into()); + assert_eq!(group_data.minted, 0_u32.into()); + assert_eq!(group_data.retired, 0_u32.into()); + + let group_data = stored_data.batch_groups.get(&1u32).unwrap(); + assert_eq!(group_data.batches, get_multiple_batch_list::()); + assert_eq!(group_data.name, new_batch.name); + assert_eq!(group_data.uuid, new_batch.uuid); + assert_eq!(group_data.total_supply, 200_u32.into()); + assert_eq!(group_data.minted, 0_u32.into()); + assert_eq!(group_data.retired, 0_u32.into()); + }); +} diff --git a/pallets/dex/Cargo.toml b/pallets/dex/Cargo.toml index ec634363..cf492b2c 100644 --- a/pallets/dex/Cargo.toml +++ b/pallets/dex/Cargo.toml @@ -15,6 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ "derive", ] } +log = { version = "0.4.17", default-features = false } scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.33" } frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.33" } @@ -47,7 +48,8 @@ std = [ "pallet-balances/std", "primitives/std", "orml-tokens/std", - "pallet-carbon-credits/std" + "pallet-carbon-credits/std", + "log/std" ] runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dex/src/benchmarking.rs b/pallets/dex/src/benchmarking.rs index 9b1a1fb8..40145d1b 100644 --- a/pallets/dex/src/benchmarking.rs +++ b/pallets/dex/src/benchmarking.rs @@ -76,7 +76,7 @@ benchmarks! { assert!(Orders::::get(0u128).is_none()) } - buy_order { + create_buy_order { create_default_minted_asset::(true, 100u32.into()); let caller: T::AccountId = whitelisted_caller(); Dex::::create_sell_order(RawOrigin::Signed(caller.clone()).into(), 0u32.into(), 100u32.into(), 1u32.into())?; diff --git a/pallets/dex/src/lib.rs b/pallets/dex/src/lib.rs index 31996be0..3fa439a3 100644 --- a/pallets/dex/src/lib.rs +++ b/pallets/dex/src/lib.rs @@ -42,30 +42,21 @@ mod benchmarking; mod weights; pub use weights::WeightInfo; - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] -pub struct OrderInfo { - owner: AccountId, - units: AssetBalance, - price_per_unit: TokenBalance, - asset_id: AssetId, -} - -pub type OrderId = u128; +mod types; #[frame_support::pallet] pub mod pallet { - use crate::{OrderId, OrderInfo, WeightInfo}; + use crate::{types::*, WeightInfo}; use frame_support::{ pallet_prelude::*, - traits::fungibles::{Inspect, Transfer}, + traits::{fungibles::Transfer, Contains}, transactional, PalletId, }; use frame_system::pallet_prelude::{OriginFor, *}; use orml_traits::MultiCurrency; use primitives::CarbonCreditsValidator; use sp_runtime::{ - traits::{AccountIdConversion, AtLeast32BitUnsigned, CheckedSub, One, Zero}, + traits::{AccountIdConversion, AtLeast32BitUnsigned, CheckedAdd, CheckedSub, One, Zero}, Percent, }; @@ -73,23 +64,6 @@ pub mod pallet { #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); - pub type CurrencyBalanceOf = - <::Currency as MultiCurrency<::AccountId>>::Balance; - - pub type CurrencyIdOf = <::Currency as MultiCurrency< - ::AccountId, - >>::CurrencyId; - - pub type AssetBalanceOf = - <::Asset as Inspect<::AccountId>>::Balance; - - pub type AssetIdOf = - <::Asset as Inspect<::AccountId>>::AssetId; - - pub type ProjectIdOf = <::AssetValidator as CarbonCreditsValidator>::ProjectId; - - pub type GroupIdOf = <::AssetValidator as CarbonCreditsValidator>::GroupId; - /// Configure the pallet by specifying the parameters and types on which it depends. #[pallet::config] pub trait Config: frame_system::Config { @@ -130,10 +104,6 @@ pub mod pallet { /// Verify if the asset can be listed on the dex type AssetValidator: CarbonCreditsValidator>; - /// The CurrencyId of the stable currency we accept as payment - #[pallet::constant] - type StableCurrencyId: Get>; - /// The minimum units of asset to create a sell order #[pallet::constant] type MinUnitsToCreateSellOrder: Get>; @@ -156,6 +126,18 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + /// The maximum validators for a payment + type MaxValidators: Get + TypeInfo + Clone; + + /// The maximum length of tx hash that can be stored on chain + type MaxTxHashLen: Get + TypeInfo + Clone; + + /// KYC provider config + type KYCProvider: Contains; + + /// The expiry time for buy order + type BuyOrderExpiryTime: Get; } // orders information @@ -163,6 +145,11 @@ pub mod pallet { #[pallet::getter(fn order_count)] pub type OrderCount = StorageValue<_, OrderId, ValueQuery>; + // orders information + #[pallet::storage] + #[pallet::getter(fn buy_order_count)] + pub type BuyOrderCount = StorageValue<_, BuyOrderId, ValueQuery>; + // Payment fees charged by dex #[pallet::storage] #[pallet::getter(fn payment_fees)] @@ -175,12 +162,26 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn order_info)] - pub type Orders = StorageMap< - _, - Blake2_128Concat, - OrderId, - OrderInfo, AssetBalanceOf, CurrencyBalanceOf>, - >; + pub type Orders = StorageMap<_, Blake2_128Concat, OrderId, OrderInfoOf>; + + #[pallet::storage] + #[pallet::getter(fn buy_order_info)] + pub type BuyOrders = StorageMap<_, Blake2_128Concat, BuyOrderId, BuyOrderInfoOf>; + + #[pallet::storage] + #[pallet::getter(fn validator_accounts)] + // List of ValidatorAccounts for the pallet + pub type ValidatorAccounts = StorageValue<_, ValidatorAccountsListOf, ValueQuery>; + + #[pallet::type_value] + pub fn DefaultMinPaymentValidators() -> u32 { + 2u32 + } + // Min validations required before a payment is accepted + #[pallet::storage] + #[pallet::getter(fn min_payment_validators)] + pub type MinPaymentValidations = + StorageValue<_, u32, ValueQuery, DefaultMinPaymentValidators>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -198,16 +199,25 @@ pub mod pallet { /// A sell order was cancelled SellOrderCancelled { order_id: OrderId, seller: T::AccountId }, /// A buy order was processed successfully - BuyOrderFilled { + BuyOrderCreated { order_id: OrderId, units: AssetBalanceOf, project_id: ProjectIdOf, group_id: GroupIdOf, price_per_unit: CurrencyBalanceOf, fees_paid: CurrencyBalanceOf, + total_amount: CurrencyBalanceOf, seller: T::AccountId, buyer: T::AccountId, }, + /// A new ValidatorAccount has been added + ValidatorAccountAdded { account_id: T::AccountId }, + /// An ValidatorAccount has been removed + ValidatorAccountRemoved { account_id: T::AccountId }, + /// A buy order payment was validated + BuyOrderPaymentValidated { order_id: BuyOrderId, chain_id: u32, validator: T::AccountId }, + /// A buy order was completed successfully + BuyOrderCompleted { order_id: BuyOrderId }, } // Errors inform users that something went wrong. @@ -241,6 +251,70 @@ pub mod pallet { FeeExceedsUserLimit, /// The purchasea fee amount exceeds the limit CannotSetMoreThanMaxPurchaseFee, + /// not authorized to perform action + NotAuthorised, + ValidatorAccountAlreadyExists, + TooManyValidatorAccounts, + ChainIdMismatch, + TxProofMismatch, + KYCAuthorisationFailed, + DuplicateValidation, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + // Look for expired buy orders and remove from storage + fn on_idle(block: T::BlockNumber, remaining_weight: Weight) -> Weight { + let mut remaining_weight = remaining_weight; + for (key, buy_order) in BuyOrders::::iter() { + remaining_weight = remaining_weight.saturating_sub(T::DbWeight::get().reads(1)); + if buy_order.expiry_time < block { + // log the start of removal + log::info!( + target: "runtime::dex", + "INFO: Found expired buy order, going to remove buy_order_id: {}", + key + ); + BuyOrders::::take(key); + remaining_weight = + remaining_weight.saturating_sub(T::DbWeight::get().writes(1)); + // add the credits to the sell order + let sell_order_updated = Orders::::try_mutate( + buy_order.order_id, + |maybe_order| -> DispatchResult { + let order = maybe_order.as_mut().ok_or(Error::::InvalidOrderId)?; + order.units = order + .units + .checked_add(&buy_order.units) + .ok_or(Error::::OrderUnitsOverflow)?; + Ok(()) + }, + ); + + if sell_order_updated.is_err() { + log::warn!( + target: "runtime::dex", + "WARNING: Sell order units not credited back for buy_order_id: {}", + key + ); + } + + log::info!( + target: "runtime::dex", + "INFO: Removed Expired buy order with buy_order_id: {}", + key + ); + + // exit since we altered the map + break + } + + if remaining_weight.all_lte(T::DbWeight::get().reads(1)) { + break + } + } + remaining_weight + } } #[pallet::call] @@ -255,7 +329,7 @@ pub mod pallet { price_per_unit: CurrencyBalanceOf, ) -> DispatchResult { let seller = ensure_signed(origin.clone())?; - + Self::check_kyc_approval(&seller)?; // ensure the asset_id can be listed let (project_id, group_id) = T::AssetValidator::get_project_details(&asset_id) .ok_or(Error::::AssetNotPermitted)?; @@ -316,16 +390,18 @@ pub mod pallet { } /// Buy `units` of `asset_id` from the given `order_id` + /// This will be called by one of the approved validators when an order is created #[transactional] #[pallet::weight(T::WeightInfo::buy_order())] - pub fn buy_order( + pub fn create_buy_order( origin: OriginFor, order_id: OrderId, asset_id: AssetIdOf, units: AssetBalanceOf, max_fee: CurrencyBalanceOf, ) -> DispatchResult { - let buyer = ensure_signed(origin.clone())?; + let buyer = ensure_signed(origin)?; + Self::check_kyc_approval(&buyer)?; if units.is_zero() { return Ok(()) @@ -365,45 +441,54 @@ pub mod pallet { let purchase_fee: u128 = PurchaseFees::::get().try_into().map_err(|_| Error::::ArithmeticError)?; - let required_fees = + let total_fee = payment_fee.checked_add(purchase_fee).ok_or(Error::::OrderUnitsOverflow)?; - ensure!(max_fee >= required_fees.into(), Error::::FeeExceedsUserLimit); - - // send purchase price to seller - T::Currency::transfer( - T::StableCurrencyId::get(), - &buyer, - &order.owner, - required_currency.into(), - )?; - - // transfer fee to pallet - T::Currency::transfer( - T::StableCurrencyId::get(), - &buyer, - &Self::account_id(), - required_fees.into(), - )?; - - // transfer asset to buyer - T::Asset::transfer(order.asset_id, &Self::account_id(), &buyer, units, false)?; - - Self::deposit_event(Event::BuyOrderFilled { + let total_amount = total_fee + .checked_add(required_currency) + .ok_or(Error::::OrderUnitsOverflow)?; + + ensure!(max_fee >= total_fee.into(), Error::::FeeExceedsUserLimit); + + // Create buy order + let buy_order_id = Self::buy_order_count(); + let next_buy_order_id = + buy_order_id.checked_add(One::one()).ok_or(Error::::OrderIdOverflow)?; + BuyOrderCount::::put(next_buy_order_id); + + let current_block_number = >::block_number(); + let expiry_time = current_block_number + .checked_add(&T::BuyOrderExpiryTime::get()) + .ok_or(Error::::OrderIdOverflow)?; + + BuyOrders::::insert( + buy_order_id, + BuyOrderInfo { + order_id, + buyer: buyer.clone(), + units, + price_per_unit: order.price_per_unit, + asset_id, + total_fee: total_fee.into(), + total_amount: total_amount.into(), + expiry_time, + payment_info: None, + }, + ); + + Self::deposit_event(Event::BuyOrderCreated { order_id, units, project_id, group_id, price_per_unit: order.price_per_unit, - fees_paid: required_fees.into(), + fees_paid: total_fee.into(), + total_amount: total_amount.into(), seller: order.owner.clone(), buyer, }); - // remove the sell order if all units are filled - if !order.units.is_zero() { - *maybe_order = Some(order) - } + *maybe_order = Some(order); Ok(()) }) @@ -439,6 +524,137 @@ pub mod pallet { PurchaseFees::::set(purchase_fee); Ok(()) } + + /// Buy `units` of `asset_id` from the given `order_id` + /// This will be called by one of the approved validators when an order is created + #[transactional] + #[pallet::weight(T::WeightInfo::buy_order())] + pub fn validate_buy_order( + origin: OriginFor, + order_id: BuyOrderId, + chain_id: u32, + tx_proof: BoundedVec, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_validator_account(&sender)?; + + // fetch the buy order + BuyOrders::::try_mutate(order_id, |maybe_order| -> DispatchResult { + let mut order = maybe_order.take().ok_or(Error::::InvalidOrderId)?; + + let mut payment_info = order.payment_info.clone(); + + // if paymentInfo exists, validate against existing payment + if let Some(mut payment_info) = payment_info { + ensure!(payment_info.chain_id == chain_id, Error::::ChainIdMismatch); + ensure!(payment_info.tx_proof == tx_proof, Error::::TxProofMismatch); + ensure!( + !payment_info.validators.contains(&sender), + Error::::DuplicateValidation + ); + + payment_info + .validators + .try_push(sender.clone()) + .map_err(|_| Error::::TooManyValidatorAccounts)?; + + order.payment_info = Some(payment_info.clone()); + + Self::deposit_event(Event::BuyOrderPaymentValidated { + order_id, + chain_id, + validator: sender.clone(), + }); + + // process payment if we have reached threshold + if payment_info.validators.len() as u32 >= Self::min_payment_validators() { + // transfer the asset to the buyer + T::Asset::transfer( + order.asset_id, + &Self::account_id(), + &order.buyer, + order.units, + false, + )?; + + Self::deposit_event(Event::BuyOrderCompleted { order_id }); + + // remove from storage if we reached the threshold and payment executed + return Ok(()) + } + + *maybe_order = Some(order); + + Ok(()) + } + // else if paymentInfo is empty create it + else { + let mut validators: BoundedVec = + Default::default(); + validators + .try_push(sender.clone()) + .map_err(|_| Error::::TooManyValidatorAccounts)?; + payment_info = Some(PaymentInfo { chain_id, tx_proof, validators }); + + order.payment_info = payment_info; + + Self::deposit_event(Event::BuyOrderPaymentValidated { + order_id, + chain_id, + validator: sender.clone(), + }); + + *maybe_order = Some(order); + + Ok(()) + } + }) + } + + /// Add a new account to the list of authorised Accounts + /// The caller must be from a permitted origin + #[transactional] + #[pallet::weight(T::WeightInfo::force_set_purchase_fee())] + pub fn force_add_validator_account( + origin: OriginFor, + account_id: T::AccountId, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + // add the account_id to the list of authorized accounts + ValidatorAccounts::::try_mutate(|account_list| -> DispatchResult { + ensure!( + !account_list.contains(&account_id), + Error::::ValidatorAccountAlreadyExists + ); + + account_list + .try_push(account_id.clone()) + .map_err(|_| Error::::TooManyValidatorAccounts)?; + Ok(()) + })?; + + Self::deposit_event(Event::ValidatorAccountAdded { account_id }); + Ok(()) + } + + /// Remove an account from the list of authorised accounts + #[transactional] + #[pallet::weight(T::WeightInfo::force_set_purchase_fee())] + pub fn force_remove_validator_account( + origin: OriginFor, + account_id: T::AccountId, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + // remove the account_id from the list of authorized accounts if already exists + ValidatorAccounts::::try_mutate(|account_list| -> DispatchResult { + if let Ok(index) = account_list.binary_search(&account_id) { + account_list.swap_remove(index); + Self::deposit_event(Event::ValidatorAccountRemoved { account_id }); + } + + Ok(()) + }) + } } impl Pallet { @@ -446,5 +662,24 @@ pub mod pallet { pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() } + + /// Checks if the given account_id is part of authorized account list + pub fn check_validator_account(account_id: &T::AccountId) -> DispatchResult { + let validator_accounts = ValidatorAccounts::::get(); + if !validator_accounts.contains(account_id) { + Err(Error::::NotAuthorised.into()) + } else { + Ok(()) + } + } + + /// Checks if given account is kyc approved + pub fn check_kyc_approval(account_id: &T::AccountId) -> DispatchResult { + if !T::KYCProvider::contains(account_id) { + Err(Error::::KYCAuthorisationFailed.into()) + } else { + Ok(()) + } + } } } diff --git a/pallets/dex/src/mock.rs b/pallets/dex/src/mock.rs index 91d589c2..068a2676 100644 --- a/pallets/dex/src/mock.rs +++ b/pallets/dex/src/mock.rs @@ -144,13 +144,30 @@ impl CarbonCreditsValidator for DummyValidator { } } +pub struct MockKycProvider; +impl Contains for MockKycProvider { + fn contains(value: &u64) -> bool { + // special account to test negative kyc + if value == &20 { + return false + } + + true + } +} + parameter_types! { pub const DexPalletId: PalletId = PalletId(*b"bitg/dex"); - pub StableCurrencyId: CurrencyId = CurrencyId::USDT; pub const MinUnitsToCreateSellOrder : u32 = 2; pub const MinPricePerUnit : u32 = 1; pub const MaxPaymentFee : Percent = Percent::from_percent(50); pub const MaxPurchaseFee : u128 = 100u128; + #[derive(Clone, scale_info::TypeInfo)] + pub const MaxValidators : u32 = 10; + #[derive(Clone, scale_info::TypeInfo)] + pub const MaxTxHashLen : u32 = 100; + #[derive(Clone, scale_info::TypeInfo)] + pub const BuyOrderExpiryTime : u32 = 2; } impl pallet_dex::Config for Test { @@ -159,10 +176,13 @@ impl pallet_dex::Config for Test { type Currency = Tokens; type CurrencyBalance = u128; type AssetBalance = u128; - type StableCurrencyId = StableCurrencyId; type PalletId = DexPalletId; + type KYCProvider = MockKycProvider; type MinPricePerUnit = MinPricePerUnit; type AssetValidator = DummyValidator; + type MaxValidators = MaxValidators; + type MaxTxHashLen = MaxTxHashLen; + type BuyOrderExpiryTime = BuyOrderExpiryTime; type MinUnitsToCreateSellOrder = MinUnitsToCreateSellOrder; type ForceOrigin = EnsureRoot; type MaxPaymentFee = MaxPaymentFee; diff --git a/pallets/dex/src/tests.rs b/pallets/dex/src/tests.rs index 26e8b965..52a3af3c 100644 --- a/pallets/dex/src/tests.rs +++ b/pallets/dex/src/tests.rs @@ -1,11 +1,19 @@ // This file is part of BitGreen. // Copyright (C) 2022 BitGreen. // This code is licensed under MIT license (see LICENSE.txt for details) -use crate::{mock::*, Error, Event, Orders}; -use frame_support::{assert_noop, assert_ok, PalletId}; -use orml_traits::MultiCurrency; +use crate::{mock::*, BuyOrders, Error, Event, Orders}; +use frame_support::{ + assert_noop, assert_ok, traits::OnIdle, weights::Weight, BoundedVec, PalletId, +}; +use frame_system::RawOrigin; use sp_runtime::{traits::AccountIdConversion, Percent}; +/// helper function to add authorised account +fn add_validator_account(validator_account: u64) { + // authorise the account + assert_ok!(Dex::force_add_validator_account(RawOrigin::Root.into(), validator_account)); +} + #[test] fn basic_create_sell_order_should_work() { new_test_ext().execute_with(|| { @@ -169,50 +177,50 @@ fn buy_order_should_work() { // non existing order should fail assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(buyer), 10, 0, 1, 100), + Dex::create_buy_order(RuntimeOrigin::signed(buyer), 10, 0, 1, 100), Error::::InvalidOrderId ); + // non kyc buyer should fail + assert_noop!( + Dex::create_buy_order(RuntimeOrigin::signed(20), 0, 10, 1, 100), + Error::::KYCAuthorisationFailed + ); + // non matching asset_id should fail assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(buyer), 0, 10, 1, 100), + Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, 10, 1, 100), Error::::InvalidAssetId ); // more than listed volume should fail assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1000, 100), + Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1000, 100), Error::::OrderUnitsOverflow ); - // should fail if the user does not have enough balance - assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(5), 0, asset_id, 1, 100), - orml_tokens::Error::::BalanceTooLow - ); - // should fail if the buyer and seller are same assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(1), 0, asset_id, 1, 100), + Dex::create_buy_order(RuntimeOrigin::signed(seller), 0, asset_id, 1, 100), Error::::SellerAndBuyerCannotBeSame ); // should fail if the fee is zero assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 0), + Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 0), Error::::FeeExceedsUserLimit ); // should fail if the fee is less than expected assert_noop!( - Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 0), + Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 0), Error::::FeeExceedsUserLimit ); // use should be able to purchase - assert_ok!(Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 11)); + assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 11)); - // storage should be updated correctly + // sell order storage should be updated correctly let sell_order_storage = Orders::::get(0).unwrap(); assert_eq!(sell_order_storage.owner, seller); assert_eq!(sell_order_storage.units, 4); @@ -220,27 +228,31 @@ fn buy_order_should_work() { assert_eq!(sell_order_storage.asset_id, asset_id); // Asset balance should be set correctly + // no transfers until payment is validated assert_eq!(Assets::balance(asset_id, seller), 95); - assert_eq!(Assets::balance(asset_id, buyer), 1); - assert_eq!(Assets::balance(asset_id, dex_account), 4); + assert_eq!(Assets::balance(asset_id, buyer), 0); + assert_eq!(Assets::balance(asset_id, dex_account), 5); - // Token balance should be set correctly - // seller gets the price_per_unit - assert_eq!(Tokens::free_balance(USDT, &seller), 10); - // buyer spends price_per_unit + fees (10 + 1 + 10) - assert_eq!(Tokens::free_balance(USDT, &buyer), 79); - // pallet gets fees (1 + 10) - assert_eq!(Tokens::free_balance(USDT, &dex_account), 11); + // buy order storage should be updated correctly + let buy_order_storage = BuyOrders::::get(0).unwrap(); + assert_eq!(buy_order_storage.buyer, buyer); + assert_eq!(buy_order_storage.units, 1); + assert_eq!(buy_order_storage.price_per_unit, 10); + assert_eq!(buy_order_storage.asset_id, asset_id); + assert_eq!(buy_order_storage.total_fee, 11); + assert_eq!(buy_order_storage.total_amount, 21); + assert!(buy_order_storage.payment_info.is_none()); assert_eq!( last_event(), - Event::BuyOrderFilled { + Event::BuyOrderCreated { order_id: 0, units: 1, price_per_unit: 10, seller, buyer, fees_paid: 11u128, + total_amount: 21u128, project_id: 0, group_id: 0, } @@ -250,12 +262,13 @@ fn buy_order_should_work() { } #[test] -fn sell_order_is_removed_if_all_units_bought() { +fn validate_buy_order_should_work() { new_test_ext().execute_with(|| { let asset_id = 0; let seller = 1; let buyer = 4; - let dex_account: u64 = PalletId(*b"bitg/dex").into_account_truncating(); + let validator = 10; + let buy_order_id = 0; assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(seller), asset_id, 1, 100)); @@ -268,28 +281,62 @@ fn sell_order_is_removed_if_all_units_bought() { // should be able to create a sell order assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 5, 10)); - // user should be able to purchase - assert_ok!(Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 5, 100)); + // create a new buy order + assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 11)); - // sell order should be removed since all units have been bought - assert!(Orders::::get(0).is_none()); + let tx_proof: BoundedVec<_, _> = vec![].try_into().unwrap(); - // Token balance should be set correctly - // seller gets the price_per_unit - assert_eq!(Tokens::free_balance(USDT, &seller), 50); - // buyer spends price_per_unit + fees (50 + 5 + 10) - assert_eq!(Tokens::free_balance(USDT, &buyer), 35); - // pallet gets fees (5 + 10) - assert_eq!(Tokens::free_balance(USDT, &dex_account), 15); + // non validator cannot validate + assert_noop!( + Dex::validate_buy_order( + RuntimeOrigin::signed(buyer), + buy_order_id, + 0u32, + vec![].try_into().unwrap() + ), + Error::::NotAuthorised + ); + + // validator can validate a payment order + add_validator_account(validator); + assert_ok!(Dex::validate_buy_order( + RuntimeOrigin::signed(validator), + buy_order_id, + 0u32, + tx_proof.clone() + )); + + // buy order storage should be updated correctly + let buy_order_storage = BuyOrders::::get(0).unwrap(); + assert_eq!(buy_order_storage.buyer, buyer); + assert_eq!(buy_order_storage.units, 1); + assert_eq!(buy_order_storage.price_per_unit, 10); + assert_eq!(buy_order_storage.asset_id, asset_id); + assert_eq!(buy_order_storage.total_fee, 11); + assert_eq!(buy_order_storage.total_amount, 21); + + let payment_info = buy_order_storage.payment_info.unwrap(); + assert_eq!(payment_info.chain_id, 0u32); + assert_eq!(payment_info.tx_proof, tx_proof); + assert_eq!(payment_info.validators.len(), 1); + assert_eq!(payment_info.validators.first().unwrap(), &validator); + + assert_eq!( + last_event(), + Event::BuyOrderPaymentValidated { order_id: 0, chain_id: 0u32, validator }.into() + ); }); } #[test] -fn partial_fill_and_cancel_works() { +fn payment_is_processed_after_validator_threshold_reached() { new_test_ext().execute_with(|| { let asset_id = 0; let seller = 1; let buyer = 4; + let validator = 10; + let validator_two = 11; + let buy_order_id = 0; let dex_account: u64 = PalletId(*b"bitg/dex").into_account_truncating(); assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1)); @@ -301,30 +348,128 @@ fn partial_fill_and_cancel_works() { assert_ok!(Dex::force_set_purchase_fee(RuntimeOrigin::root(), 10u32.into())); // should be able to create a sell order - assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 50, 10)); + assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 5, 10)); - // user should be able to purchase - assert_ok!(Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 5, 100)); + add_validator_account(validator); + add_validator_account(validator_two); - // cancel sell order should return the remaining units - assert_ok!(Dex::cancel_sell_order(RuntimeOrigin::signed(seller), 0)); + // create a new buy order + assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 11)); - // Balance should be returned correctly - assert_eq!(Assets::balance(asset_id, seller), 95); - assert_eq!(Assets::balance(asset_id, dex_account), 0); + let tx_proof: BoundedVec<_, _> = vec![].try_into().unwrap(); - assert_eq!(last_event(), Event::SellOrderCancelled { order_id: 0, seller }.into()); + // non validator cannot validate + assert_noop!( + Dex::validate_buy_order( + RuntimeOrigin::signed(buyer), + buy_order_id, + 0u32, + vec![].try_into().unwrap() + ), + Error::::NotAuthorised + ); + + // validator can validate a payment order + assert_ok!(Dex::validate_buy_order( + RuntimeOrigin::signed(validator), + buy_order_id, + 0u32, + tx_proof.clone() + )); + + // buy order storage should be updated correctly + let buy_order_storage = BuyOrders::::get(0).unwrap(); + assert_eq!(buy_order_storage.buyer, buyer); + assert_eq!(buy_order_storage.units, 1); + assert_eq!(buy_order_storage.price_per_unit, 10); + assert_eq!(buy_order_storage.asset_id, asset_id); + assert_eq!(buy_order_storage.total_fee, 11); + assert_eq!(buy_order_storage.total_amount, 21); + + let payment_info = buy_order_storage.payment_info.unwrap(); + assert_eq!(payment_info.chain_id, 0u32); + assert_eq!(payment_info.tx_proof, tx_proof); + assert_eq!(payment_info.validators.len(), 1); + assert_eq!(payment_info.validators.first().unwrap(), &validator); + + assert_eq!( + last_event(), + Event::BuyOrderPaymentValidated { order_id: 0, chain_id: 0u32, validator }.into() + ); + + // same validator cannot validate again + assert_noop!( + Dex::validate_buy_order( + RuntimeOrigin::signed(validator), + buy_order_id, + 0u32, + tx_proof.clone() + ), + Error::::DuplicateValidation + ); + + // next validator validates + assert_ok!(Dex::validate_buy_order( + RuntimeOrigin::signed(validator_two), + buy_order_id, + 0u32, + tx_proof + )); - // Token balance should be set correctly - // seller gets the price_per_unit - assert_eq!(Tokens::free_balance(USDT, &seller), 50); - // buyer spends price_per_unit + fees (50 + 5 + 10) - assert_eq!(Tokens::free_balance(USDT, &buyer), 35); - // pallet gets fees (5 + 10) - assert_eq!(Tokens::free_balance(USDT, &dex_account), 15); + // buy order storage should be cleared since payment is done + let buy_order_storage = BuyOrders::::get(0); + assert!(buy_order_storage.is_none()); + + // Asset balance should be set correctly + assert_eq!(Assets::balance(asset_id, seller), 95); + assert_eq!(Assets::balance(asset_id, buyer), 1); + assert_eq!(Assets::balance(asset_id, dex_account), 4); + + assert_eq!(last_event(), Event::BuyOrderCompleted { order_id: 0 }.into()); }); } +// #[test] +// fn partial_fill_and_cancel_works() { +// new_test_ext().execute_with(|| { +// let asset_id = 0; +// let seller = 1; +// let buyer = 4; +// let dex_account: u64 = PalletId(*b"bitg/dex").into_account_truncating(); + +// assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1)); +// assert_ok!(Assets::mint(RuntimeOrigin::signed(seller), asset_id, 1, 100)); +// assert_eq!(Assets::balance(asset_id, seller), 100); + +// // set fee values +// assert_ok!(Dex::force_set_payment_fee(RuntimeOrigin::root(), Percent::from_percent(10))); +// assert_ok!(Dex::force_set_purchase_fee(RuntimeOrigin::root(), 10u32.into())); + +// // should be able to create a sell order +// assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 50, 10)); + +// // user should be able to purchase +// assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 5, 100)); + +// // cancel sell order should return the remaining units +// assert_ok!(Dex::cancel_sell_order(RuntimeOrigin::signed(seller), 0)); + +// // Balance should be returned correctly +// assert_eq!(Assets::balance(asset_id, seller), 95); +// assert_eq!(Assets::balance(asset_id, dex_account), 0); + +// assert_eq!(last_event(), Event::SellOrderCancelled { order_id: 0, seller }.into()); + +// // Token balance should be set correctly +// // seller gets the price_per_unit +// assert_eq!(Tokens::free_balance(USDT, &seller), 50); +// // buyer spends price_per_unit + fees (50 + 5 + 10) +// assert_eq!(Tokens::free_balance(USDT, &buyer), 35); +// // pallet gets fees (5 + 10) +// assert_eq!(Tokens::free_balance(USDT, &dex_account), 15); +// }); +// } + #[test] fn cannot_set_more_than_max_fee() { new_test_ext().execute_with(|| { @@ -337,39 +482,86 @@ fn cannot_set_more_than_max_fee() { }); } +// #[test] +// fn fee_is_more_expensive_when_order_is_split() { +// new_test_ext().execute_with(|| { +// // Assuming a 75 price, and fees at 10%; if a user buys 50 units they’ll pay 750 in fees +// // If they instead have 50 separate orders of 1 unit each, they should pay 775 in fees +// // Here we assume purchase fee is zero, since this is to test the payment fee calculation +// let asset_id = 0; +// let seller = 1; +// let buyer = 10; +// let dex_account: u64 = PalletId(*b"bitg/dex").into_account_truncating(); + +// assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1)); +// assert_ok!(Assets::mint(RuntimeOrigin::signed(seller), asset_id, 1, 100)); +// assert_eq!(Assets::balance(asset_id, seller), 100); + +// // set fee values +// assert_ok!(Dex::force_set_payment_fee(RuntimeOrigin::root(), Percent::from_percent(10))); +// assert_ok!(Dex::force_set_purchase_fee(RuntimeOrigin::root(), 0u32.into())); + +// // should be able to create a sell order +// assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 100, 75)); + +// // Let the user make a single purchase of 50 units +// assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 50, 1000)); +// // pallet gets fees (10%) +// assert_eq!(Tokens::free_balance(USDT, &dex_account), 375); + +// // Let the user make a purchse of 1 unit 50 times +// for _i in 0..50 { +// assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 1000)); +// } + +// // pallet gets more than 20% (750) +// assert_eq!(Tokens::free_balance(USDT, &dex_account), 775); +// }); +// } + #[test] -fn fee_is_more_expensive_when_order_is_split() { +fn buy_order_handle_expiry_should_work() { new_test_ext().execute_with(|| { - // Assuming a 75 price, and fees at 10%; if a user buys 50 units they’ll pay 750 in fees - // If they instead have 50 separate orders of 1 unit each, they should pay 775 in fees - // Here we assume purchase fee is zero, since this is to test the payment fee calculation let asset_id = 0; let seller = 1; - let buyer = 10; - let dex_account: u64 = PalletId(*b"bitg/dex").into_account_truncating(); + let buyer = 4; + let validator = 10; assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(seller), asset_id, 1, 100)); assert_eq!(Assets::balance(asset_id, seller), 100); - // set fee values - assert_ok!(Dex::force_set_payment_fee(RuntimeOrigin::root(), Percent::from_percent(10))); - assert_ok!(Dex::force_set_purchase_fee(RuntimeOrigin::root(), 0u32.into())); - // should be able to create a sell order - assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 100, 75)); + assert_ok!(Dex::create_sell_order(RuntimeOrigin::signed(seller), asset_id, 5, 10)); + + add_validator_account(validator); + + // use should be able to purchase + assert_ok!(Dex::create_buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 11)); + + // sell order storage should be updated correctly + let sell_order_storage = Orders::::get(0).unwrap(); + assert_eq!(sell_order_storage.owner, seller); + assert_eq!(sell_order_storage.units, 4); + assert_eq!(sell_order_storage.price_per_unit, 10); + assert_eq!(sell_order_storage.asset_id, asset_id); - // Let the user make a single purchase of 50 units - assert_ok!(Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 50, 1000)); - // pallet gets fees (10%) - assert_eq!(Tokens::free_balance(USDT, &dex_account), 375); + // buy order storage should be updated correctly + let buy_order_storage = BuyOrders::::get(0).unwrap(); + assert_eq!(buy_order_storage.buyer, buyer); + assert_eq!(buy_order_storage.units, 1); + assert_eq!(buy_order_storage.expiry_time, 3); - // Let the user make a purchse of 1 unit 50 times - for _i in 0..50 { - assert_ok!(Dex::buy_order(RuntimeOrigin::signed(buyer), 0, asset_id, 1, 1000)); - } + Dex::on_idle(5, Weight::MAX); - // pallet gets more than 20% (750) - assert_eq!(Tokens::free_balance(USDT, &dex_account), 775); + // the order should be cleared + assert!(BuyOrders::::get(0).is_none()); + + // sell order storage should be restored correctly + let sell_order_storage = Orders::::get(0).unwrap(); + assert_eq!(sell_order_storage.owner, seller); + assert_eq!(sell_order_storage.units, 5); + assert_eq!(sell_order_storage.price_per_unit, 10); + assert_eq!(sell_order_storage.asset_id, asset_id); }); } diff --git a/pallets/dex/src/types.rs b/pallets/dex/src/types.rs new file mode 100644 index 00000000..f6556f8e --- /dev/null +++ b/pallets/dex/src/types.rs @@ -0,0 +1,79 @@ +use super::*; +use frame_support::{traits::fungibles::Inspect, BoundedVec}; +use orml_traits::MultiCurrency; +use primitives::CarbonCreditsValidator; +use sp_runtime::traits::Get; + +pub type CurrencyBalanceOf = + <::Currency as MultiCurrency<::AccountId>>::Balance; + +pub type AssetBalanceOf = + <::Asset as Inspect<::AccountId>>::Balance; + +pub type AssetIdOf = + <::Asset as Inspect<::AccountId>>::AssetId; + +pub type ProjectIdOf = <::AssetValidator as CarbonCreditsValidator>::ProjectId; + +pub type GroupIdOf = <::AssetValidator as CarbonCreditsValidator>::GroupId; + +/// ValidatorAccounts type of pallet +pub type ValidatorAccountsListOf = + BoundedVec<::AccountId, ::MaxValidators>; + +pub type OrderInfoOf = OrderInfo< + ::AccountId, + AssetIdOf, + AssetBalanceOf, + CurrencyBalanceOf, +>; + +pub type BuyOrderInfoOf = BuyOrderInfo< + ::AccountId, + AssetIdOf, + AssetBalanceOf, + CurrencyBalanceOf, + ::BlockNumber, + ::MaxTxHashLen, + ::MaxValidators, +>; + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] +pub struct OrderInfo { + pub owner: AccountId, + pub units: AssetBalance, + pub price_per_unit: TokenBalance, + pub asset_id: AssetId, +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] +pub struct BuyOrderInfo< + AccountId, + AssetId, + AssetBalance, + TokenBalance, + Time, + TxProofLen: Get + Clone, + MaxValidators: Get + Clone, +> { + pub order_id: OrderId, + pub buyer: AccountId, + pub units: AssetBalance, + pub price_per_unit: TokenBalance, + pub asset_id: AssetId, + pub total_fee: TokenBalance, + pub total_amount: TokenBalance, + pub expiry_time: Time, + pub payment_info: Option>, +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] +pub struct PaymentInfo + Clone, MaxValidators: Get + Clone> { + pub chain_id: u32, + pub tx_proof: BoundedVec, + pub validators: BoundedVec, +} + +pub type OrderId = u128; + +pub type BuyOrderId = u128; diff --git a/pallets/kyc/Cargo.toml b/pallets/kyc/Cargo.toml new file mode 100644 index 00000000..24db48fb --- /dev/null +++ b/pallets/kyc/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "pallet-kyc" +version = "0.0.1" +authors = ["Bitgreen"] +edition = "2021" +homepage = 'https://bitgreen.org' +license = 'MIT' +description = "Pallet to handle onchain KYC validation" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", optional = true, default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } +sp-io = {git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "pallet-balances/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/kyc/README.md b/pallets/kyc/README.md new file mode 100644 index 00000000..3499a3f8 --- /dev/null +++ b/pallets/kyc/README.md @@ -0,0 +1,6 @@ +# Membership Module + +Allows control of membership of a set of `AccountId`s, useful for managing membership of a +collective. A prime member may be set. + +License: Apache-2.0 diff --git a/pallets/kyc/src/lib.rs b/pallets/kyc/src/lib.rs new file mode 100644 index 00000000..ecab9fd6 --- /dev/null +++ b/pallets/kyc/src/lib.rs @@ -0,0 +1,903 @@ +// This file is part of BitGreen. +// Copyright (C) 2022 BitGreen. +// This code is licensed under MIT license (see LICENSE.txt for details) +// +//! # KYC Module +//! +//! Allows control of membership of a set of `AccountId`s, useful for managing membership of a +//! collective. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + traits::{ + ChangeMembers, Contains, Currency, ExistenceRequirement, Get, InitializeMembers, + SortedMembers, + }, + BoundedVec, PalletId, +}; +use sp_runtime::traits::{AccountIdConversion, StaticLookup}; +use sp_std::prelude::*; +pub mod weights; + +pub use pallet::*; +pub use weights::WeightInfo; + +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData<(T, I)>); + + pub type BalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// Required origin for adding a member (though can always be Root). + type AddOrigin: EnsureOrigin; + + /// The receiver of the signal for when the membership has been initialized. This happens + /// pre-genesis and will usually be the same as `MembershipChanged`. If you need to do + /// something different on initialization, then you can change this accordingly. + type MembershipInitialized: InitializeMembers; + + /// The receiver of the signal for when the membership has changed. + type MembershipChanged: ChangeMembers; + + /// The maximum number of members that this membership can have. + /// + /// This is used for benchmarking. Re-run the benchmarks if this changes. + /// + /// This is enforced in the code; the membership size can not exceed this limit. + type MaxMembers: Get; + + /// Maximum amount of authorised accounts permitted + type MaxAuthorizedAccountCount: Get; + + /// The currency used for the pallet + type Currency: Currency; + + /// The KYC pallet id + #[pallet::constant] + type PalletId: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// The current membership, stored as an ordered Vec. + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn authorized_accounts)] + // List of AuthorizedAccounts for the pallet + pub type AuthorizedAccounts, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn airdrop_amount)] + // Amount to airdrop on every kyc success + pub type AirdropAmount, I: 'static = ()> = StorageValue<_, BalanceOf>; + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub members: BoundedVec, + pub phantom: PhantomData, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { members: Default::default(), phantom: Default::default() } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + use sp_std::collections::btree_set::BTreeSet; + let members_set: BTreeSet<_> = self.members.iter().collect(); + assert_eq!( + members_set.len(), + self.members.len(), + "Members cannot contain duplicate accounts." + ); + + let mut members = self.members.clone(); + members.sort(); + T::MembershipInitialized::initialize_members(&members); + >::put(members); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// The given member was added + MemberAdded { who: T::AccountId }, + /// The given member was removed + MemberRemoved { who: T::AccountId }, + /// Two members were swapped; see the transaction for who. + MembersSwapped, + /// The membership was reset; see the transaction for who the new set is. + MembersReset, + /// One of the members' keys changed. + KeyChanged, + /// Phantom member, never used. + Dummy { _phantom_data: PhantomData<(T::AccountId, >::RuntimeEvent)> }, + /// A new AuthorizedAccount has been added + AuthorizedAccountAdded { account_id: T::AccountId }, + /// An AuthorizedAccount has been removed + AuthorizedAccountRemoved { account_id: T::AccountId }, + /// User has received airdrop for kyc approval + KYCAirdrop { who: T::AccountId, amount: BalanceOf }, + } + + #[pallet::error] + pub enum Error { + /// Already a member. + AlreadyMember, + /// Not a member. + NotMember, + /// Too many members. + TooManyMembers, + /// Adding a new authorized account failed + TooManyAuthorizedAccounts, + /// Cannot add a duplicate authorised account + AuthorizedAccountAlreadyExists, + /// No authorization account + NotAuthorised, + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Add a member `who` to the set. + /// + /// May only be called from `T::AddOrigin`. + #[pallet::call_index(0)] + #[pallet::weight(50_000_000)] + pub fn add_member(origin: OriginFor, who: AccountIdLookupOf) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; + let who = T::Lookup::lookup(who)?; + + let mut members = >::get(); + let location = members.binary_search(&who).err().ok_or(Error::::AlreadyMember)?; + members + .try_insert(location, who.clone()) + .map_err(|_| Error::::TooManyMembers)?; + + >::put(&members); + + T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members[..]); + + let _ = Self::transfer_kyc_airdrop(who.clone()); + + Self::deposit_event(Event::MemberAdded { who }); + Ok(()) + } + + /// Remove a member `who` from the set. + /// + /// May only be called from `T::RemoveOrigin`. + #[pallet::call_index(1)] + #[pallet::weight(50_000_000)] + pub fn remove_member(origin: OriginFor, who: AccountIdLookupOf) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; + + let who = T::Lookup::lookup(who)?; + + let mut members = >::get(); + let location = members.binary_search(&who).ok().ok_or(Error::::NotMember)?; + members.remove(location); + + >::put(&members); + + T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]); + + Self::deposit_event(Event::MemberRemoved { who }); + Ok(()) + } + + /// Swap out one member `remove` for another `add`. + /// + /// May only be called from `T::SwapOrigin`. + /// + /// Prime membership is *not* passed from `remove` to `add`, if extant. + #[pallet::call_index(2)] + #[pallet::weight(50_000_000)] + pub fn swap_member( + origin: OriginFor, + remove: AccountIdLookupOf, + add: AccountIdLookupOf, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; + + let remove = T::Lookup::lookup(remove)?; + let add = T::Lookup::lookup(add)?; + + if remove == add { + return Ok(()) + } + + let mut members = >::get(); + let location = members.binary_search(&remove).ok().ok_or(Error::::NotMember)?; + let _ = members.binary_search(&add).err().ok_or(Error::::AlreadyMember)?; + members[location] = add.clone(); + members.sort(); + + >::put(&members); + + T::MembershipChanged::change_members_sorted(&[add], &[remove], &members[..]); + + Self::deposit_event(Event::MembersSwapped); + Ok(()) + } + + /// Change the membership to a new set, disregarding the existing membership. Be nice and + /// pass `members` pre-sorted. + /// + /// May only be called from `T::ResetOrigin`. + #[pallet::call_index(3)] + #[pallet::weight(50_000_000)] + pub fn reset_members(origin: OriginFor, members: Vec) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; + + let mut members: BoundedVec = + BoundedVec::try_from(members).map_err(|_| Error::::TooManyMembers)?; + members.sort(); + >::mutate(|m| { + T::MembershipChanged::set_members_sorted(&members[..], m); + *m = members; + }); + + Self::deposit_event(Event::MembersReset); + Ok(()) + } + + /// Swap out the sending member for some other key `new`. + /// + /// May only be called from `Signed` origin of a current member. + /// + /// Prime membership is passed from the origin account to `new`, if extant. + #[pallet::call_index(4)] + #[pallet::weight(50_000_000)] + pub fn change_key(origin: OriginFor, new: AccountIdLookupOf) -> DispatchResult { + let remove = ensure_signed(origin)?; + let new = T::Lookup::lookup(new)?; + + if remove != new { + let mut members = >::get(); + let location = + members.binary_search(&remove).ok().ok_or(Error::::NotMember)?; + let _ = members.binary_search(&new).err().ok_or(Error::::AlreadyMember)?; + members[location] = new.clone(); + members.sort(); + + >::put(&members); + + T::MembershipChanged::change_members_sorted(&[new], &[remove], &members[..]); + } + + Self::deposit_event(Event::KeyChanged); + Ok(()) + } + + /// Add a new account to the list of authorised Accounts + /// The caller must be from a permitted origin + #[pallet::call_index(5)] + #[pallet::weight(50_000_000)] + pub fn force_add_authorized_account( + origin: OriginFor, + account_id: T::AccountId, + ) -> DispatchResult { + T::AddOrigin::ensure_origin(origin)?; + // add the account_id to the list of authorized accounts + AuthorizedAccounts::::try_mutate(|account_list| -> DispatchResult { + ensure!( + !account_list.contains(&account_id), + Error::::AuthorizedAccountAlreadyExists + ); + + account_list + .try_push(account_id.clone()) + .map_err(|_| Error::::TooManyAuthorizedAccounts)?; + Ok(()) + })?; + + Self::deposit_event(Event::AuthorizedAccountAdded { account_id }); + Ok(()) + } + + /// Remove an account from the list of authorised accounts + #[pallet::call_index(6)] + #[pallet::weight(50_000_000)] + pub fn force_remove_authorized_account( + origin: OriginFor, + account_id: T::AccountId, + ) -> DispatchResult { + T::AddOrigin::ensure_origin(origin)?; + // remove the account_id from the list of authorized accounts if already exists + AuthorizedAccounts::::try_mutate(|account_list| -> DispatchResult { + if let Ok(index) = account_list.binary_search(&account_id) { + account_list.swap_remove(index); + Self::deposit_event(Event::AuthorizedAccountRemoved { account_id }); + } + + Ok(()) + }) + } + + /// Set the airdrop amount for each successful kyc + #[pallet::call_index(7)] + #[pallet::weight(50_000_000)] + pub fn force_set_kyc_airdrop( + origin: OriginFor, + amount: Option>, + ) -> DispatchResult { + T::AddOrigin::ensure_origin(origin)?; + // remove the account_id from the list of authorized accounts if already exists + AirdropAmount::::set(amount); + Ok(()) + } + } +} + +impl, I: 'static> Pallet { + /// Checks if the given account_id is part of authorized account list + pub fn check_authorized_account( + account_id: &T::AccountId, + ) -> frame_support::pallet_prelude::DispatchResult { + let authorized_accounts = AuthorizedAccounts::::get(); + if !authorized_accounts.contains(account_id) { + Err(Error::::NotAuthorised.into()) + } else { + Ok(()) + } + } + + /// The account ID of the KYC pallet + pub fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Airdrop native tokens to user + pub fn transfer_kyc_airdrop( + who: T::AccountId, + ) -> frame_support::pallet_prelude::DispatchResult { + // transfer airdrop if the amount is set + if let Some(amount) = Self::airdrop_amount() { + let airdrop_executed = T::Currency::transfer( + &Self::account_id(), + &who, + amount, + ExistenceRequirement::AllowDeath, + ); + + if airdrop_executed.is_ok() { + Self::deposit_event(Event::KYCAirdrop { who, amount }); + } + } + Ok(()) + } +} + +impl, I: 'static> Contains for Pallet { + fn contains(t: &T::AccountId) -> bool { + Self::members().binary_search(t).is_ok() + } +} + +impl, I: 'static> SortedMembers for Pallet { + fn sorted_members() -> Vec { + Self::members().to_vec() + } + + fn count() -> usize { + Members::::decode_len().unwrap_or(0) + } +} + +#[cfg(feature = "runtime-benchmarks")] +mod benchmark { + use super::{Pallet as Membership, *}; + use frame_benchmarking::v1::{account, benchmarks_instance_pallet, whitelist, BenchmarkError}; + use frame_support::{assert_ok, traits::EnsureOrigin}; + use frame_system::RawOrigin; + + const SEED: u32 = 0; + + fn set_members, I: 'static>(members: Vec, prime: Option) { + let reset_origin = T::ResetOrigin::try_successful_origin() + .expect("ResetOrigin has no successful origin required for the benchmark"); + let prime_origin = T::PrimeOrigin::try_successful_origin() + .expect("PrimeOrigin has no successful origin required for the benchmark"); + + assert_ok!(>::reset_members(reset_origin, members.clone())); + if let Some(prime) = prime.map(|i| members[i].clone()) { + let prime_lookup = T::Lookup::unlookup(prime); + assert_ok!(>::set_prime(prime_origin, prime_lookup)); + } else { + assert_ok!(>::clear_prime(prime_origin)); + } + } + + benchmarks_instance_pallet! { + add_member { + let m in 1 .. (T::MaxMembers::get() - 1); + + let members = (0..m).map(|i| account("member", i, SEED)).collect::>(); + set_members::(members, None); + let new_member = account::("add", m, SEED); + let new_member_lookup = T::Lookup::unlookup(new_member.clone()); + }: { + assert_ok!(>::add_member( + T::AddOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, + new_member_lookup, + )); + } verify { + assert!(>::get().contains(&new_member)); + #[cfg(test)] crate::tests::clean(); + } + + // the case of no prime or the prime being removed is surely cheaper than the case of + // reporting a new prime via `MembershipChanged`. + remove_member { + let m in 2 .. T::MaxMembers::get(); + + let members = (0..m).map(|i| account("member", i, SEED)).collect::>(); + set_members::(members.clone(), Some(members.len() - 1)); + + let to_remove = members.first().cloned().unwrap(); + let to_remove_lookup = T::Lookup::unlookup(to_remove.clone()); + }: { + assert_ok!(>::remove_member( + T::RemoveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, + to_remove_lookup, + )); + } verify { + assert!(!>::get().contains(&to_remove)); + // prime is rejigged + assert!(>::get().is_some() && T::MembershipChanged::get_prime().is_some()); + #[cfg(test)] crate::tests::clean(); + } + + // we remove a non-prime to make sure it needs to be set again. + swap_member { + let m in 2 .. T::MaxMembers::get(); + + let members = (0..m).map(|i| account("member", i, SEED)).collect::>(); + set_members::(members.clone(), Some(members.len() - 1)); + let add = account::("member", m, SEED); + let add_lookup = T::Lookup::unlookup(add.clone()); + let remove = members.first().cloned().unwrap(); + let remove_lookup = T::Lookup::unlookup(remove.clone()); + }: { + assert_ok!(>::swap_member( + T::SwapOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, + remove_lookup, + add_lookup, + )); + } verify { + assert!(!>::get().contains(&remove)); + assert!(>::get().contains(&add)); + // prime is rejigged + assert!(>::get().is_some() && T::MembershipChanged::get_prime().is_some()); + #[cfg(test)] crate::tests::clean(); + } + + // er keep the prime common between incoming and outgoing to make sure it is rejigged. + reset_member { + let m in 1 .. T::MaxMembers::get(); + + let members = (1..m+1).map(|i| account("member", i, SEED)).collect::>(); + set_members::(members.clone(), Some(members.len() - 1)); + let mut new_members = (m..2*m).map(|i| account("member", i, SEED)).collect::>(); + }: { + assert_ok!(>::reset_members( + T::ResetOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, + new_members.clone(), + )); + } verify { + new_members.sort(); + assert_eq!(>::get(), new_members); + // prime is rejigged + assert!(>::get().is_some() && T::MembershipChanged::get_prime().is_some()); + #[cfg(test)] crate::tests::clean(); + } + + change_key { + let m in 1 .. T::MaxMembers::get(); + + // worse case would be to change the prime + let members = (0..m).map(|i| account("member", i, SEED)).collect::>(); + let prime = members.last().cloned().unwrap(); + set_members::(members.clone(), Some(members.len() - 1)); + + let add = account::("member", m, SEED); + let add_lookup = T::Lookup::unlookup(add.clone()); + whitelist!(prime); + }: { + assert_ok!(>::change_key(RawOrigin::Signed(prime.clone()).into(), add_lookup)); + } verify { + assert!(!>::get().contains(&prime)); + assert!(>::get().contains(&add)); + // prime is rejigged + assert_eq!(>::get().unwrap(), add); + #[cfg(test)] crate::tests::clean(); + } + + set_prime { + let m in 1 .. T::MaxMembers::get(); + let members = (0..m).map(|i| account("member", i, SEED)).collect::>(); + let prime = members.last().cloned().unwrap(); + let prime_lookup = T::Lookup::unlookup(prime.clone()); + set_members::(members, None); + }: { + assert_ok!(>::set_prime( + T::PrimeOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, + prime_lookup, + )); + } verify { + assert!(>::get().is_some()); + assert!(::get_prime().is_some()); + #[cfg(test)] crate::tests::clean(); + } + + clear_prime { + let m in 1 .. T::MaxMembers::get(); + let members = (0..m).map(|i| account("member", i, SEED)).collect::>(); + let prime = members.last().cloned().unwrap(); + set_members::(members, None); + }: { + assert_ok!(>::clear_prime( + T::PrimeOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, + )); + } verify { + assert!(>::get().is_none()); + assert!(::get_prime().is_none()); + #[cfg(test)] crate::tests::clean(); + } + + impl_benchmark_test_suite!(Membership, crate::tests::new_bench_ext(), crate::tests::Test); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as pallet_membership; + use frame_system::RawOrigin; + + use sp_core::H256; + use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + }; + + use frame_support::{ + assert_noop, assert_ok, bounded_vec, ord_parameter_types, parameter_types, + traits::{ConstU32, ConstU64, GenesisBuild}, + }; + + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + type Block = frame_system::mocking::MockBlock; + + frame_support::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}, + Membership: pallet_membership::{Pallet, Call, Storage, Config, Event}, + } + ); + + parameter_types! { + pub const ExistentialDeposit: u64 = 1; + } + + impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); + } + + parameter_types! { + pub static Members: Vec = vec![]; + pub static Prime: Option = None; + pub const KycPalletId: PalletId = PalletId(*b"bitg/kyc"); + } + + impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type RuntimeCall = RuntimeCall; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; + } + ord_parameter_types! { + pub const One: u64 = 1; + pub const Two: u64 = 2; + pub const Three: u64 = 3; + pub const Four: u64 = 4; + pub const Five: u64 = 5; + } + + pub struct TestChangeMembers; + impl ChangeMembers for TestChangeMembers { + fn change_members_sorted(incoming: &[u64], outgoing: &[u64], new: &[u64]) { + let mut old_plus_incoming = Members::get(); + old_plus_incoming.extend_from_slice(incoming); + old_plus_incoming.sort(); + let mut new_plus_outgoing = new.to_vec(); + new_plus_outgoing.extend_from_slice(outgoing); + new_plus_outgoing.sort(); + assert_eq!(old_plus_incoming, new_plus_outgoing); + + Members::set(new.to_vec()); + Prime::set(None); + } + fn set_prime(who: Option) { + Prime::set(who); + } + fn get_prime() -> Option { + Prime::get() + } + } + + impl InitializeMembers for TestChangeMembers { + fn initialize_members(members: &[u64]) { + MEMBERS.with(|m| *m.borrow_mut() = members.to_vec()); + } + } + + impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = frame_system::EnsureRoot; + type MembershipInitialized = TestChangeMembers; + type MembershipChanged = TestChangeMembers; + type MaxMembers = ConstU32<10>; + type MaxAuthorizedAccountCount = ConstU32<10>; + type PalletId = KycPalletId; + type Currency = Balances; + type WeightInfo = (); + } + + pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + // We use default for brevity, but you can configure as desired if needed. + pallet_membership::GenesisConfig:: { + members: bounded_vec![10, 20, 30], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn new_bench_ext() -> sp_io::TestExternalities { + frame_system::GenesisConfig::default().build_storage::().unwrap().into() + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn clean() { + Members::set(vec![]); + Prime::set(None); + } + + #[test] + fn query_membership_works() { + new_test_ext().execute_with(|| { + assert_eq!(Membership::members(), vec![10, 20, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), vec![10, 20, 30]); + }); + } + + #[test] + fn add_member_works() { + new_test_ext().execute_with(|| { + let authorised_account = 1; + assert_ok!(Membership::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account, + )); + assert_noop!( + Membership::add_member(RuntimeOrigin::signed(5), 15), + crate::Error::::NotAuthorised + ); + assert_noop!( + Membership::add_member(RuntimeOrigin::signed(authorised_account), 10), + Error::::AlreadyMember + ); + assert_ok!(Membership::add_member(RuntimeOrigin::signed(authorised_account), 15)); + assert_eq!(Membership::members(), vec![10, 15, 20, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + }); + } + + #[test] + fn add_member_airdrop_works() { + new_test_ext().execute_with(|| { + let authorised_account = 1; + assert_ok!(Membership::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account, + )); + + // set the airdrop amount + let airdrop_amount = 10; + assert_ok!(Membership::force_set_kyc_airdrop( + RawOrigin::Root.into(), + Some(airdrop_amount), + )); + + // set some balance to the pallet account + let kyc_pallet_account: u64 = PalletId(*b"bitg/kyc").into_account_truncating(); + Balances::make_free_balance_be(&kyc_pallet_account, 100); + + let balance_before_kyc = Balances::free_balance(&15); + assert_ok!(Membership::add_member(RuntimeOrigin::signed(authorised_account), 15)); + assert_eq!(Membership::members(), vec![10, 15, 20, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + assert_eq!(Balances::free_balance(&15), balance_before_kyc + airdrop_amount); + }); + } + + #[test] + fn remove_member_works() { + new_test_ext().execute_with(|| { + let authorised_account = 1; + assert_ok!(Membership::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account, + )); + assert_noop!( + Membership::remove_member(RuntimeOrigin::signed(5), 20), + Error::::NotAuthorised + ); + assert_noop!( + Membership::remove_member(RuntimeOrigin::signed(authorised_account), 15), + Error::::NotMember + ); + assert_ok!(Membership::remove_member(RuntimeOrigin::signed(authorised_account), 20)); + assert_eq!(Membership::members(), vec![10, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + }); + } + + #[test] + fn swap_member_works() { + new_test_ext().execute_with(|| { + let authorised_account = 1; + assert_ok!(Membership::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account, + )); + assert_noop!( + Membership::swap_member(RuntimeOrigin::signed(5), 10, 25), + Error::::NotAuthorised + ); + assert_noop!( + Membership::swap_member(RuntimeOrigin::signed(authorised_account), 15, 25), + Error::::NotMember + ); + assert_noop!( + Membership::swap_member(RuntimeOrigin::signed(authorised_account), 10, 30), + Error::::AlreadyMember + ); + + assert_ok!(Membership::swap_member(RuntimeOrigin::signed(authorised_account), 20, 20)); + assert_eq!(Membership::members(), vec![10, 20, 30]); + + assert_ok!(Membership::swap_member(RuntimeOrigin::signed(authorised_account), 10, 25)); + assert_eq!(Membership::members(), vec![20, 25, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + }); + } + + #[test] + fn swap_member_works_that_does_not_change_order() { + new_test_ext().execute_with(|| { + let authorised_account = 1; + assert_ok!(Membership::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account, + )); + assert_ok!(Membership::swap_member(RuntimeOrigin::signed(authorised_account), 10, 5)); + assert_eq!(Membership::members(), vec![5, 20, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + }); + } + + #[test] + fn change_key_works() { + new_test_ext().execute_with(|| { + let authorised_account = 1; + assert_ok!(Membership::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account, + )); + assert_noop!( + Membership::change_key(RuntimeOrigin::signed(3), 25), + Error::::NotMember + ); + assert_noop!( + Membership::change_key(RuntimeOrigin::signed(10), 20), + Error::::AlreadyMember + ); + assert_ok!(Membership::change_key(RuntimeOrigin::signed(10), 40)); + assert_eq!(Membership::members(), vec![20, 30, 40]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + }); + } + + #[test] + fn change_key_works_that_does_not_change_order() { + new_test_ext().execute_with(|| { + assert_ok!(Membership::change_key(RuntimeOrigin::signed(10), 5)); + assert_eq!(Membership::members(), vec![5, 20, 30]); + assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members().to_vec()); + }); + } + + #[test] + #[should_panic(expected = "Members cannot contain duplicate accounts.")] + fn genesis_build_panics_with_duplicate_members() { + pallet_membership::GenesisConfig:: { + members: bounded_vec![1, 2, 3, 1], + phantom: Default::default(), + } + .build_storage() + .unwrap(); + } +} diff --git a/pallets/kyc/src/weights.rs b/pallets/kyc/src/weights.rs new file mode 100644 index 00000000..31c359b4 --- /dev/null +++ b/pallets/kyc/src/weights.rs @@ -0,0 +1,364 @@ +// This file is part of Substrate. + +// Copyright (C) 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_membership +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-03-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_membership +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/membership/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![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_membership. +pub trait WeightInfo { + fn add_member(m: u32, ) -> Weight; + fn remove_member(m: u32, ) -> Weight; + fn swap_member(m: u32, ) -> Weight; + fn reset_member(m: u32, ) -> Weight; + fn change_key(m: u32, ) -> Weight; + fn set_prime(m: u32, ) -> Weight; + fn clear_prime(m: u32, ) -> Weight; +} + +/// Weights for pallet_membership using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 99]`. + fn add_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `174 + m * (64 ±0)` + // Estimated: `6691 + m * (192 ±0)` + // Minimum execution time: 17_587_000 picoseconds. + Weight::from_parts(18_658_163, 6691) + // Standard Error: 710 + .saturating_add(Weight::from_parts(46_294, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:0) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[2, 100]`. + fn remove_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 20_402_000 picoseconds. + Weight::from_parts(21_165_819, 8520) + // Standard Error: 643 + .saturating_add(Weight::from_parts(45_481, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:0) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[2, 100]`. + fn swap_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 20_380_000 picoseconds. + Weight::from_parts(21_633_260, 8520) + // Standard Error: 770 + .saturating_add(Weight::from_parts(55_504, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:0) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn reset_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 19_989_000 picoseconds. + Weight::from_parts(22_352_059, 8520) + // Standard Error: 2_878 + .saturating_add(Weight::from_parts(156_367, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:1) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn change_key(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 21_275_000 picoseconds. + Weight::from_parts(23_344_594, 8520) + // Standard Error: 2_750 + .saturating_add(Weight::from_parts(46_736, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:0) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalMembership Prime (r:0 w:1) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn set_prime(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `32 + m * (32 ±0)` + // Estimated: `4719 + m * (32 ±0)` + // Minimum execution time: 8_087_000 picoseconds. + Weight::from_parts(8_909_627, 4719) + // Standard Error: 1_572 + .saturating_add(Weight::from_parts(17_186, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Prime (r:0 w:1) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn clear_prime(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_752_000 picoseconds. + Weight::from_parts(4_081_144, 0) + // Standard Error: 229 + .saturating_add(Weight::from_parts(1_298, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 99]`. + fn add_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `174 + m * (64 ±0)` + // Estimated: `6691 + m * (192 ±0)` + // Minimum execution time: 17_587_000 picoseconds. + Weight::from_parts(18_658_163, 6691) + // Standard Error: 710 + .saturating_add(Weight::from_parts(46_294, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:0) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[2, 100]`. + fn remove_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 20_402_000 picoseconds. + Weight::from_parts(21_165_819, 8520) + // Standard Error: 643 + .saturating_add(Weight::from_parts(45_481, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:0) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[2, 100]`. + fn swap_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 20_380_000 picoseconds. + Weight::from_parts(21_633_260, 8520) + // Standard Error: 770 + .saturating_add(Weight::from_parts(55_504, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:0) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn reset_member(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 19_989_000 picoseconds. + Weight::from_parts(22_352_059, 8520) + // Standard Error: 2_878 + .saturating_add(Weight::from_parts(156_367, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:1) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Proposals (r:1 w:0) + /// Proof Skipped: TechnicalCommittee Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalMembership Prime (r:1 w:1) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Members (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn change_key(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278 + m * (64 ±0)` + // Estimated: `8520 + m * (192 ±0)` + // Minimum execution time: 21_275_000 picoseconds. + Weight::from_parts(23_344_594, 8520) + // Standard Error: 2_750 + .saturating_add(Weight::from_parts(46_736, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_parts(0, 192).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Members (r:1 w:0) + /// Proof: TechnicalMembership Members (max_values: Some(1), max_size: Some(3202), added: 3697, mode: MaxEncodedLen) + /// Storage: TechnicalMembership Prime (r:0 w:1) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn set_prime(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `32 + m * (32 ±0)` + // Estimated: `4719 + m * (32 ±0)` + // Minimum execution time: 8_087_000 picoseconds. + Weight::from_parts(8_909_627, 4719) + // Standard Error: 1_572 + .saturating_add(Weight::from_parts(17_186, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 32).saturating_mul(m.into())) + } + /// Storage: TechnicalMembership Prime (r:0 w:1) + /// Proof: TechnicalMembership Prime (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) + /// Storage: TechnicalCommittee Prime (r:0 w:1) + /// Proof Skipped: TechnicalCommittee Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 100]`. + fn clear_prime(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_752_000 picoseconds. + Weight::from_parts(4_081_144, 0) + // Standard Error: 229 + .saturating_add(Weight::from_parts(1_298, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } +} diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index 74460b2c..c4c9ffc1 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -75,8 +75,8 @@ pub mod pallet { use frame_system::{pallet_prelude::*, Config as SystemConfig}; use pallet_session::SessionManager; use sp_runtime::{ - traits::{CheckedAdd, CheckedDiv, Convert}, - Percent, + traits::{CheckedAdd, Convert}, + FixedPointNumber, Percent, }; use sp_staking::SessionIndex; use sp_std::fmt::Debug; @@ -309,6 +309,8 @@ pub mod pallet { NoUnbondingDelegation, /// The unbonding delay has not been reached UnbondingDelayNotPassed, + /// Already delegated + AlreadyDelegated, } #[pallet::hooks] @@ -477,6 +479,17 @@ pub mod pallet { // add the delegator to the list of delegators let delegation_info = DelegationInfoOf:: { who: who.clone(), deposit: amount }; + // ensure not already delegated + ensure!( + candidate + .delegators + .clone() + .into_inner() + .binary_search_by(|v| { v.who.cmp(&who) }) + .is_err(), + Error::::AlreadyDelegated + ); + candidate .delegators .try_push(delegation_info) @@ -711,10 +724,10 @@ pub mod pallet { match Candidates::::get().into_iter().find(|c| c.who == who) { Some(candidate) => Some((candidate, false)), // also search in invulnerable list if not found - None => match Invulnerables::::get().into_iter().find(|c| c.who == who) { - Some(candidate) => Some((candidate, true)), - None => None, - }, + None => Invulnerables::::get() + .into_iter() + .find(|c| c.who == who) + .map(|candidate| (candidate, true)), } } @@ -766,6 +779,19 @@ pub mod pallet { collators } + /// Calculate the total stake of the given delegator set + pub fn sum_delegator_set_stake( + set: &BoundedVec, T::MaxDelegators>, + ) -> BalanceOf { + let mut delegators_total_stake: BalanceOf = Default::default(); + + for delegator in set.iter() { + delegators_total_stake = + delegators_total_stake.checked_add(&delegator.deposit).unwrap_or_default(); + } + delegators_total_stake + } + /// Kicks out candidates that did not produce a block in the kick threshold /// and refund their deposits. pub fn kick_stale_candidates( @@ -808,34 +834,54 @@ pub mod pallet { let fee_reward = T::Currency::free_balance(&pot) .checked_sub(&T::Currency::minimum_balance()) .unwrap_or_else(Zero::zero); + log::info!("fee_reward {:?}", fee_reward); // add inflation rewards to the parachain_staking_pot let reward = Self::inflation_reward_per_block() .checked_add(&fee_reward) .unwrap_or_else(Zero::zero); + log::info!("total_reward {:?}", reward); // fetch the candidate details for the author if let Some((mut candidate, is_invulnerable)) = Self::get_candidate(author.clone()) { if !candidate.delegators.is_empty() { // total delegator reward is 90% let delegator_reward = Percent::from_percent(90).mul_floor(reward); - let reward_for_one_delegator = delegator_reward - .checked_div(&(candidate.delegators.len() as u32).into()) - .unwrap_or_default(); let delegator_data = candidate.delegators.clone(); + let delegators_total_stake: BalanceOf = + Self::sum_delegator_set_stake(&delegator_data); + log::info!("delegators_total_stake {:?}", delegators_total_stake); + let mut new_delegator_data: Vec> = Default::default(); for mut delegator in delegator_data.into_iter() { + // reward users based on the share of stake they hold compared to the pool + let delegator_deposit_as_u128: u128 = + delegator.deposit.try_into().unwrap_or_default(); + let delegators_total_stake_as_u128: u128 = + delegators_total_stake.try_into().unwrap_or_default(); + let delegator_share_of_total_stake = sp_runtime::FixedU128::from_rational( + delegator_deposit_as_u128, + delegators_total_stake_as_u128, + ); + log::info!( + "delegator_share_of_total_stake {:?}", + delegator_share_of_total_stake + ); + + let reward_for_delegator = delegator_share_of_total_stake + .checked_mul_int(delegator_reward) + .unwrap_or_default(); + log::info!( + "reward_for_delegator {:?} is {:?}", + delegator.who, + reward_for_delegator + ); + // update the delegator stake with the reward amount - delegator.deposit = - delegator.deposit.saturating_add(reward_for_one_delegator); + delegator.deposit = delegator.deposit.saturating_add(reward_for_delegator); new_delegator_data.push(delegator); - - // Self::deposit_event(Event::DelegatorRewardsTransferred { - // account_id: delegator.who.clone(), - // amount: reward_for_one_delegator, - // }); } // this should not fail because the bounds are the same @@ -844,24 +890,15 @@ pub mod pallet { // send rest of reward to collator let collator_reward = Percent::from_percent(10).mul_floor(reward); candidate.deposit = candidate.deposit.saturating_add(collator_reward); - candidate.total_stake = candidate.total_stake.saturating_add(reward); - // Self::deposit_event(Event::CollatorRewardsTransferred { - // account_id: author.clone(), - // amount: collator_reward, - // }); + candidate.total_stake = candidate.total_stake.saturating_add(reward); } else { // `reward` pot account minus ED, this should never fail. candidate.deposit = candidate.deposit.saturating_add(reward); candidate.total_stake = candidate.total_stake.saturating_add(reward); - - // Self::deposit_event(Event::CollatorRewardsTransferred { - // account_id: author.clone(), - // amount: reward, - // }); } - let _ = Self::update_candidate(candidate.clone(), is_invulnerable); + let _ = Self::update_candidate(candidate, is_invulnerable); } >::insert(author, frame_system::Pallet::::block_number()); diff --git a/pallets/parachain-staking/src/migration.rs b/pallets/parachain-staking/src/migration.rs index 082cff4f..ff90634c 100644 --- a/pallets/parachain-staking/src/migration.rs +++ b/pallets/parachain-staking/src/migration.rs @@ -1,52 +1,41 @@ use super::*; -pub mod v2 { +pub mod v3 { use super::*; - use crate::types::CandidateInfoOf; + use crate::types::{CandidateInfoOf, DelegationInfoOf}; use frame_support::{ - migration, pallet_prelude::Weight, traits::{Get, OnRuntimeUpgrade}, BoundedVec, }; - use sp_std::vec::Vec; + use sp_runtime::Saturating; + use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; - pub struct MigrateToV2(sp_std::marker::PhantomData); - impl OnRuntimeUpgrade for MigrateToV2 { + pub struct MigrateToV3(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV3 { fn on_runtime_upgrade() -> Weight { - log::info!("MIGRATION : About to execute parachain-staking migration!"); + log::info!("MIGRATION : About to execute parachain-staking migration V3!"); // retreive the current invulnerables list - let current_validators = migration::get_storage_value::>( - b"ParachainStaking", - b"Invulnerables", - &[], - ); - - if let Some(current_validators) = current_validators { - // convert to new format - let invulnerables: BoundedVec, T::MaxInvulnerables> = - current_validators - .iter() - .cloned() - .map(|account| CandidateInfoOf:: { - who: account, - deposit: Default::default(), - delegators: Default::default(), - total_stake: Default::default(), - }) - .collect::>>() - .try_into() - .expect("current validators too large"); - - // insert new invulnerables - >::put(invulnerables.clone()); - - log::info!( - "MIGRATION : Migrated {:?} to new invulnerables format!", - invulnerables.len() - ); - } + let current_invulnerables = >::get(); + let updated_invulnerables = + Self::remove_duplicate_delegators(current_invulnerables.into_inner()); + let bounded_updated_invulnerables: BoundedVec<_, _> = + updated_invulnerables.try_into().unwrap(); + // insert new invulnerables + >::put(bounded_updated_invulnerables); + + log::info!("MIGRATION : Invulnerables migration complete!"); + + let current_candidates = >::get(); + let updated_candidates = + Self::remove_duplicate_delegators(current_candidates.into_inner()); + let bounded_updated_candidates: BoundedVec<_, _> = + updated_candidates.try_into().unwrap(); + // insert new invulnerables + >::put(bounded_updated_candidates); + + log::info!("MIGRATION : Candidates migration complete!"); T::DbWeight::get().reads_writes(2, 2) } @@ -54,8 +43,49 @@ pub mod v2 { #[cfg(feature = "try-runtime")] fn post_upgrade() -> Result<(), &'static str> { // new version must be set. - assert_eq!(Pallet::::on_chain_storage_version(), 2); + assert_eq!(Pallet::::on_chain_storage_version(), 3); Ok(()) } } + + impl MigrateToV3 { + pub fn remove_duplicate_delegators( + validators: Vec>, + ) -> Vec> { + let mut updated_validators: Vec> = Default::default(); + for mut validator in validators { + log::info!("MIGRATION : Processing validator: {:?}", validator); + let mut updated_delegators: BoundedVec, T::MaxDelegators> = + Default::default(); + let mut updated_delegators_map: BTreeMap> = + BTreeMap::new(); + for delegator in validator.delegators { + log::info!("MIGRATION : Processing delegator: {:?}", delegator); + log::info!( + "MIGRATION : Current updated delegators list: {:?}", + updated_delegators + ); + // search if the delegator exists in the updated_delegators list + if updated_delegators_map.contains_key(&delegator.who) { + if let Some(deposit) = updated_delegators_map.get_mut(&delegator.who) { + *deposit = deposit.saturating_add(delegator.deposit); + } + } else { + updated_delegators_map.insert(delegator.who, delegator.deposit); + }; + } + + for (delegator, deposit) in updated_delegators_map { + updated_delegators + .try_push(DelegationInfoOf:: { who: delegator, deposit }) + .unwrap(); + } + + validator.delegators = updated_delegators; + updated_validators.push(validator); + } + + updated_validators + } + } } diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index 9e6b2438..07af494e 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -502,6 +502,18 @@ fn delegate_works() { ); assert_ok!(CollatorSelection::delegate(RuntimeOrigin::signed(5), 3, 10)); + + // duplicate delegation should fail with different amount + assert_noop!( + CollatorSelection::delegate(RuntimeOrigin::signed(5), 3, 20), + Error::::AlreadyDelegated + ); + // duplicate delegation should fail with same amount + assert_noop!( + CollatorSelection::delegate(RuntimeOrigin::signed(5), 3, 10), + Error::::AlreadyDelegated + ); + // storage should be updated correctly let expected_delegator_info = DelegationInfoOf:: { who: 5, deposit: 10 }; assert_eq!(CollatorSelection::candidates()[0].delegators, vec![expected_delegator_info]); @@ -800,7 +812,7 @@ fn delegator_payout_works_for_invulnerables() { let collator = CandidateInfoOf:: { who: invulnerable_collator, - deposit: 0 + 15, // initial bond of 0 + 10% of reward (150) + deposit: 15, // initial bond of 0 + 10% of reward (150) delegators: vec![ // initial bond of 10 + 90% of reward (150) divided equally to two delegators DelegationInfoOf:: { who: 3u64, deposit: 10 + 67 }, /* initial bond of 10 @@ -825,3 +837,160 @@ fn delegator_payout_works_for_invulnerables() { assert_eq!(Balances::free_balance(CollatorSelection::account_id()), 105); }); } + +#[test] +fn delegator_payout_is_divided_in_correct_propotion() { + new_test_ext().execute_with(|| { + // put 100 in the pot + 5 for ED + Balances::make_free_balance_be(&CollatorSelection::account_id(), 105); + Balances::make_free_balance_be(&6, 100); + // block inflation reward is 50 + assert_ok!(CollatorSelection::set_block_inflation_reward(RuntimeOrigin::root(), 50)); + + // 4 is the default author. + assert_eq!(Balances::free_balance(4), 100); + assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4))); + // three delegators delegators to 4 + assert_ok!(CollatorSelection::delegate(RuntimeOrigin::signed(3), 4, 30)); + assert_ok!(CollatorSelection::delegate(RuntimeOrigin::signed(5), 4, 20)); + assert_ok!(CollatorSelection::delegate(RuntimeOrigin::signed(6), 4, 10)); + // triggers `note_author` + Authorship::on_initialize(4); + + // this is the expected result + let collator = CandidateInfoOf:: { + who: 4, + deposit: 10 + 15, // initial bond of 10 + 10% of reward (150) + delegators: vec![ + // initial bond of 10 + 90% of reward (135) divided in propotion of stake to 3 + // delegators + DelegationInfoOf:: { who: 3u64, deposit: 30 + 67 }, /* initial bond of 30 + * + 50% of reward + * (75) */ + DelegationInfoOf:: { who: 5u64, deposit: 20 + 44 }, /* initial bond of 10 + * + 33% of reward + * (44) */ + DelegationInfoOf:: { who: 6u64, deposit: 10 + 22 }, /* initial bond of 10 + * + 16% of reward + * (22) */ + ] + .try_into() + .unwrap(), + total_stake: 220, // initial bond of 70 + 100% of reward + }; + + assert_eq!(CollatorSelection::candidates().pop().unwrap(), collator); + assert_eq!(CollatorSelection::last_authored_block(4), 0); + + // balances should not change + assert_eq!(Balances::free_balance(4), 90); + assert_eq!(Balances::free_balance(3), 70); + assert_eq!(Balances::free_balance(5), 80); + assert_eq!(Balances::free_balance(CollatorSelection::account_id()), 105); + }); +} + +#[test] +fn test_remove_duplicate_delegators() { + use crate::{migration::v3::MigrateToV3, types::DelegationInfo}; + + let delegate_1 = DelegationInfo { who: 1, deposit: 1 }; + + let delegate_2 = DelegationInfo { who: 2, deposit: 1 }; + + let delegate_3 = DelegationInfo { who: 3, deposit: 1 }; + + let no_delegators = vec![CandidateInfoOf:: { + who: 1, + deposit: 1, + delegators: vec![].try_into().unwrap(), + total_stake: 1, + }]; + + // no change should happen + assert_eq!( + MigrateToV3::::remove_duplicate_delegators(no_delegators.clone()), + no_delegators + ); + + let no_duplicates = vec![CandidateInfoOf:: { + who: 1, + deposit: 1, + delegators: vec![delegate_1.clone(), delegate_2.clone(), delegate_3.clone()] + .try_into() + .unwrap(), + total_stake: 1, + }]; + + assert_eq!( + MigrateToV3::::remove_duplicate_delegators(no_duplicates.clone()), + no_duplicates + ); + + let some_duplicates = vec![CandidateInfoOf:: { + who: 1, + deposit: 1, + delegators: vec![ + delegate_1.clone(), + delegate_1.clone(), + delegate_2.clone(), + delegate_3.clone(), + ] + .try_into() + .unwrap(), + total_stake: 1, + }]; + + let expected_some_duplicates = vec![CandidateInfoOf:: { + who: 1, + deposit: 1, + delegators: vec![ + DelegationInfo { who: 1, deposit: 2 }, + delegate_2.clone(), + delegate_3.clone(), + ] + .try_into() + .unwrap(), + total_stake: 1, + }]; + + assert_eq!( + MigrateToV3::::remove_duplicate_delegators(some_duplicates), + expected_some_duplicates + ); + + let many_duplicates = vec![CandidateInfoOf:: { + who: 1, + deposit: 1, + delegators: vec![ + delegate_2.clone(), + delegate_3.clone(), + delegate_1.clone(), + delegate_1.clone(), + DelegationInfo { who: 1, deposit: 10 }, + delegate_3.clone(), + delegate_3.clone(), + ] + .try_into() + .unwrap(), + total_stake: 1, + }]; + + let expected_many_duplicates = vec![CandidateInfoOf:: { + who: 1, + deposit: 1, + delegators: vec![ + DelegationInfo { who: 1, deposit: 12 }, + delegate_2, + DelegationInfo { who: 3, deposit: 3 }, + ] + .try_into() + .unwrap(), + total_stake: 1, + }]; + + assert_eq!( + MigrateToV3::::remove_duplicate_delegators(many_duplicates), + expected_many_duplicates + ); +} diff --git a/pallets/vesting-contract/src/lib.rs b/pallets/vesting-contract/src/lib.rs index 4c4d2063..7e407acf 100644 --- a/pallets/vesting-contract/src/lib.rs +++ b/pallets/vesting-contract/src/lib.rs @@ -119,6 +119,12 @@ pub mod pallet { pub type BulkContractRemove = BoundedVec<::AccountId, ::MaxContractInputLength>; + /// AuthorizedAccounts type of pallet + pub type AuthorizedAccountsListOf = BoundedVec< + ::AccountId, + ::MaxAuthorizedAccountCount, + >; + /// Pallet version of balance pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -142,6 +148,9 @@ pub mod pallet { #[pallet::constant] type PalletId: Get; + /// Maximum amount of authorised accounts permitted + type MaxAuthorizedAccountCount: Get; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } @@ -162,6 +171,12 @@ pub mod pallet { // Ideally this should be equal to the pallet account balance. pub type VestingBalance = StorageValue<_, BalanceOf, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn authorized_accounts)] + // List of AuthorizedAccounts for the pallet + pub type AuthorizedAccounts = + StorageValue<_, AuthorizedAccountsListOf, ValueQuery>; + // Pallets use events to inform users when important changes are made. // https://docs.substrate.io/v3/runtime/events-and-errors #[pallet::event] @@ -173,6 +188,10 @@ pub mod pallet { ContractRemoved { recipient: T::AccountId }, /// An existing contract has been completed/withdrawn ContractWithdrawn { recipient: T::AccountId, expiry: T::BlockNumber, amount: BalanceOf }, + /// A new authorized account has been added to storage + AuthorizedAccountAdded { account_id: T::AccountId }, + /// An authorized account has been removed from storage + AuthorizedAccountRemoved { account_id: T::AccountId }, } // Errors inform users that something went wrong. @@ -190,6 +209,12 @@ pub mod pallet { ContractNotExpired, /// Contract already exists, remove old contract before adding new ContractAlreadyExists, + /// Not authorized to perform this operation + NotAuthorised, + /// Authorized account already exists + AuthorizedAccountAlreadyExists, + /// Too many authorized accounts + TooManyAuthorizedAccounts, } #[pallet::call] @@ -208,7 +233,8 @@ pub mod pallet { amount: BalanceOf, ) -> DispatchResultWithPostInfo { // ensure caller is allowed to add new recipient - T::ForceOrigin::ensure_origin(origin)?; + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; Self::do_add_new_contract(recipient, expiry, amount)?; Ok(().into()) } @@ -221,7 +247,8 @@ pub mod pallet { recipient: T::AccountId, ) -> DispatchResultWithPostInfo { // ensure caller is allowed to remove recipient - T::ForceOrigin::ensure_origin(origin)?; + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; Self::do_remove_contract(recipient)?; Ok(().into()) } @@ -235,7 +262,8 @@ pub mod pallet { recipients: BulkContractInputs, ) -> DispatchResultWithPostInfo { // ensure caller is allowed to add new recipient - T::ForceOrigin::ensure_origin(origin)?; + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; for input in recipients { Self::do_add_new_contract(input.recipient, input.expiry, input.amount)?; } @@ -251,7 +279,8 @@ pub mod pallet { recipients: BulkContractRemove, ) -> DispatchResultWithPostInfo { // ensure caller is allowed to remove recipients - T::ForceOrigin::ensure_origin(origin)?; + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; for recipient in recipients { Self::do_remove_contract(recipient)?; } @@ -286,9 +315,67 @@ pub mod pallet { recipient: T::AccountId, ) -> DispatchResultWithPostInfo { // ensure caller is allowed to force withdraw - T::ForceOrigin::ensure_origin(origin)?; + let sender = ensure_signed(origin)?; + Self::check_authorized_account(&sender)?; Self::do_withdraw_vested(recipient)?; Ok(().into()) } + + /// Add a new account to the list of authorised Accounts + /// The caller must be from a permitted origin + #[transactional] + #[pallet::weight(T::WeightInfo::force_withdraw_vested())] + pub fn force_add_authorized_account( + origin: OriginFor, + account_id: T::AccountId, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + // add the account_id to the list of authorized accounts + AuthorizedAccounts::::try_mutate(|account_list| -> DispatchResult { + ensure!( + !account_list.contains(&account_id), + Error::::AuthorizedAccountAlreadyExists + ); + + account_list + .try_push(account_id.clone()) + .map_err(|_| Error::::TooManyAuthorizedAccounts)?; + Ok(()) + })?; + + Self::deposit_event(Event::AuthorizedAccountAdded { account_id }); + Ok(()) + } + + /// Remove an account from the list of authorised accounts + #[transactional] + #[pallet::weight(T::WeightInfo::force_withdraw_vested())] + pub fn force_remove_authorized_account( + origin: OriginFor, + account_id: T::AccountId, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + // remove the account_id from the list of authorized accounts if already exists + AuthorizedAccounts::::try_mutate(|account_list| -> DispatchResult { + if let Ok(index) = account_list.binary_search(&account_id) { + account_list.swap_remove(index); + Self::deposit_event(Event::AuthorizedAccountRemoved { account_id }); + } + + Ok(()) + }) + } + } +} + +impl Pallet { + /// Checks if the given account_id is part of authorized account list + pub fn check_authorized_account(account_id: &T::AccountId) -> DispatchResult { + let authorized_accounts = AuthorizedAccounts::::get(); + if !authorized_accounts.contains(account_id) { + Err(Error::::NotAuthorised.into()) + } else { + Ok(()) + } } } diff --git a/pallets/vesting-contract/src/mock.rs b/pallets/vesting-contract/src/mock.rs index 1f928d37..2d59d4d6 100644 --- a/pallets/vesting-contract/src/mock.rs +++ b/pallets/vesting-contract/src/mock.rs @@ -79,6 +79,7 @@ impl pallet_balances::Config for Test { parameter_types! { pub const MaxContractInputLength : u32 = 10; + pub const MaxAuthorizedAccountCount : u32 = 2; pub const VestingContractPalletId: PalletId = PalletId(*b"bitg/vcp"); } @@ -88,6 +89,7 @@ impl vesting_contract::Config for Test { type ForceOrigin = frame_system::EnsureRoot; type MaxContractInputLength = MaxContractInputLength; type PalletId = VestingContractPalletId; + type MaxAuthorizedAccountCount = MaxAuthorizedAccountCount; type WeightInfo = (); } diff --git a/pallets/vesting-contract/src/tests.rs b/pallets/vesting-contract/src/tests.rs index e9433884..fd5f3d3b 100644 --- a/pallets/vesting-contract/src/tests.rs +++ b/pallets/vesting-contract/src/tests.rs @@ -2,7 +2,7 @@ // Copyright (C) 2022 BitGreen. // This code is licensed under MIT license (see LICENSE.txt for details) // -use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Currency, PalletId}; +use frame_support::{assert_noop, assert_ok, traits::Currency, PalletId}; use frame_system::RawOrigin; use sp_runtime::traits::AccountIdConversion; @@ -16,6 +16,79 @@ fn load_initial_pallet_balance(amount: u32) { Balances::make_free_balance_be(&vesting_contract_pallet_acccount, amount.into()); } +#[test] +fn add_new_authorized_accounts_should_work() { + new_test_ext().execute_with(|| { + let authorised_account_one = 1; + let authorised_account_two = 2; + let authorised_account_three = 3; + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account_one, + )); + + assert_eq!( + last_event(), + VestingContractEvent::AuthorizedAccountAdded { account_id: authorised_account_one } + .into() + ); + + assert_eq!(VestingContract::authorized_accounts().first(), Some(&authorised_account_one)); + + assert_noop!( + VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account_one, + ), + Error::::AuthorizedAccountAlreadyExists + ); + + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account_two, + )); + + assert_noop!( + VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account_three, + ), + Error::::TooManyAuthorizedAccounts + ); + + assert_eq!( + last_event(), + VestingContractEvent::AuthorizedAccountAdded { account_id: authorised_account_two } + .into() + ); + }); +} + +#[test] +fn force_remove_authorized_accounts_should_work() { + new_test_ext().execute_with(|| { + let authorised_account_one = 1; + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account_one, + )); + assert_eq!(VestingContract::authorized_accounts().first(), Some(&authorised_account_one)); + + assert_ok!(VestingContract::force_remove_authorized_account( + RawOrigin::Root.into(), + authorised_account_one, + )); + + assert_eq!( + last_event(), + VestingContractEvent::AuthorizedAccountRemoved { account_id: authorised_account_one } + .into() + ); + + assert_eq!(VestingContract::authorized_accounts().len(), 0); + }); +} + #[test] fn add_contract_fails_if_expiry_in_past() { new_test_ext().execute_with(|| { @@ -23,9 +96,16 @@ fn add_contract_fails_if_expiry_in_past() { let expiry_block = 1; let vesting_amount = 1u32; let recipient = 1; + let authorised_account = 10; + + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account + )); + assert_noop!( VestingContract::add_new_contract( - RawOrigin::Root.into(), + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, vesting_amount.into() @@ -36,7 +116,7 @@ fn add_contract_fails_if_expiry_in_past() { } #[test] -fn add_contract_fails_if_caller_not_force_origin() { +fn add_contract_fails_if_caller_not_authorized() { new_test_ext().execute_with(|| { // can only add new contract if ForceOrigin let expiry_block = 10; @@ -49,7 +129,7 @@ fn add_contract_fails_if_caller_not_force_origin() { expiry_block, vesting_amount.into() ), - BadOrigin + Error::::NotAuthorised ); }); } @@ -60,9 +140,16 @@ fn add_contract_fails_if_pallet_out_of_funds() { let expiry_block = 10; let vesting_amount = 1u32; let recipient = 1; + let authorised_account = 10; + + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account + )); + assert_noop!( VestingContract::add_new_contract( - RawOrigin::Root.into(), + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, vesting_amount.into() @@ -79,12 +166,29 @@ fn add_contract_works() { let recipient = 1; let pallet_intial_balance = 200u32; let vesting_amount = pallet_intial_balance / 2u32; + let authorised_account = 10; load_initial_pallet_balance(pallet_intial_balance); + // Should fail if unauthorised account + assert_noop!( + VestingContract::add_new_contract( + RawOrigin::Signed(20).into(), + recipient, + expiry_block, + vesting_amount.into() + ), + Error::::NotAuthorised + ); + + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account + )); + // Adding new contract works assert_ok!(VestingContract::add_new_contract( - RawOrigin::Root.into(), + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, vesting_amount.into() @@ -112,7 +216,7 @@ fn add_contract_works() { load_initial_pallet_balance(pallet_intial_balance); assert_noop!( VestingContract::add_new_contract( - RawOrigin::Root.into(), + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, (vesting_amount * 2_u32).into() @@ -131,15 +235,25 @@ fn remove_contract_works() { let vesting_amount = 1u32; load_initial_pallet_balance(pallet_intial_balance); - assert_ok!(VestingContract::add_new_contract( + let authorised_account = 10; + + assert_ok!(VestingContract::force_add_authorized_account( RawOrigin::Root.into(), + authorised_account + )); + + assert_ok!(VestingContract::add_new_contract( + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, vesting_amount.into() )); assert_eq!(VestingBalance::::get(), vesting_amount.into()); - assert_ok!(VestingContract::remove_contract(RawOrigin::Root.into(), recipient)); + assert_ok!(VestingContract::remove_contract( + RawOrigin::Signed(authorised_account).into(), + recipient + )); // contract removed from storage assert_eq!(VestingContracts::::get(recipient), None); @@ -162,8 +276,15 @@ fn withdraw_contract_works() { Error::::ContractNotFound ); - assert_ok!(VestingContract::add_new_contract( + let authorised_account = 10; + + assert_ok!(VestingContract::force_add_authorized_account( RawOrigin::Root.into(), + authorised_account + )); + + assert_ok!(VestingContract::add_new_contract( + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, vesting_amount.into() @@ -206,16 +327,25 @@ fn force_withdraw_contract_works() { let recipient = 1; let pallet_intial_balance = 100u32; let vesting_amount = 1u32; + let authorised_account = 10; load_initial_pallet_balance(pallet_intial_balance); + assert_ok!(VestingContract::force_add_authorized_account( + RawOrigin::Root.into(), + authorised_account + )); + // cannot withdraw on non existent contract assert_noop!( - VestingContract::force_withdraw_vested(RawOrigin::Root.into(), recipient), + VestingContract::force_withdraw_vested( + RawOrigin::Signed(authorised_account).into(), + recipient + ), Error::::ContractNotFound ); assert_ok!(VestingContract::add_new_contract( - RawOrigin::Root.into(), + RawOrigin::Signed(authorised_account).into(), recipient, expiry_block, vesting_amount.into() @@ -223,13 +353,19 @@ fn force_withdraw_contract_works() { // cannot withdraw before expiry assert_noop!( - VestingContract::force_withdraw_vested(RawOrigin::Root.into(), recipient), + VestingContract::force_withdraw_vested( + RawOrigin::Signed(authorised_account).into(), + recipient + ), Error::::ContractNotExpired ); // time travel to after expiry block to withdraw vested amount System::set_block_number(expiry_block + 1); - assert_ok!(VestingContract::force_withdraw_vested(RawOrigin::Root.into(), recipient)); + assert_ok!(VestingContract::force_withdraw_vested( + RawOrigin::Signed(authorised_account).into(), + recipient + )); // the user balance should be updated assert_eq!(Balances::free_balance(recipient), vesting_amount.into()); diff --git a/parachain/src/chain_spec.rs b/parachain/src/chain_spec.rs index 25c659f5..b7131761 100644 --- a/parachain/src/chain_spec.rs +++ b/parachain/src/chain_spec.rs @@ -244,7 +244,7 @@ fn testnet_genesis( polkadot_xcm: bitgreen_rococo_runtime::PolkadotXcmConfig { safe_xcm_version: Some(SAFE_XCM_VERSION), }, - kyc_membership: bitgreen_rococo_runtime::KYCMembershipConfig { + kyc: bitgreen_rococo_runtime::KYCConfig { members: [].to_vec().try_into().unwrap(), phantom: Default::default(), }, diff --git a/parachain/src/chain_spec/rococo.rs b/parachain/src/chain_spec/rococo.rs index 0856b724..272428ef 100644 --- a/parachain/src/chain_spec/rococo.rs +++ b/parachain/src/chain_spec/rococo.rs @@ -148,7 +148,7 @@ fn rococo_genesis( polkadot_xcm: bitgreen_rococo_runtime::PolkadotXcmConfig { safe_xcm_version: Some(SAFE_XCM_VERSION), }, - kyc_membership: bitgreen_rococo_runtime::KYCMembershipConfig { + kyc: bitgreen_rococo_runtime::KYCConfig { members: [].to_vec().try_into().unwrap(), phantom: Default::default(), }, diff --git a/runtime/bitgreen/Cargo.toml b/runtime/bitgreen/Cargo.toml index 03400320..03ecb225 100644 --- a/runtime/bitgreen/Cargo.toml +++ b/runtime/bitgreen/Cargo.toml @@ -99,6 +99,7 @@ pallet-carbon-credits-pool = { default-features = false, version = '0.0.1', path pallet-parachain-staking = { default-features = false, version = '0.0.1', path = "../../pallets/parachain-staking" } pallet-transaction-pause = { default-features = false, version = '0.0.1', path = "../../pallets/transaction-pause" } pallet-vesting-contract = { default-features = false, version = '0.0.1', path = "../../pallets/vesting-contract" } +pallet-kyc = { default-features = false, version = '0.0.1', path = "../../pallets/kyc" } pallet-dex = { default-features = false, path = "../../pallets/dex" } primitives = { package = "bitgreen-primitives", path = "../../primitives", default-features = false } @@ -154,6 +155,7 @@ std = [ "xcm/std", "pallet-uniques/std", "pallet-membership/std", + "pallet-kyc/std", "pallet-treasury/std", "pallet-contracts/std", "pallet-contracts-primitives/std", diff --git a/runtime/bitgreen/src/lib.rs b/runtime/bitgreen/src/lib.rs index c2fcc3a4..2481f2ec 100644 --- a/runtime/bitgreen/src/lib.rs +++ b/runtime/bitgreen/src/lib.rs @@ -112,10 +112,7 @@ pub type Executive = frame_executive::Executive< Runtime, AllPalletsWithSystem, // Migrations - ( - pallet_parachain_staking::migration::v2::MigrateToV2, - pallet_carbon_credits::migration::v1::MigrateToV1, - ), + (pallet_parachain_staking::migration::v3::MigrateToV3,), >; pub type NegativeImbalance = as Currency< @@ -178,7 +175,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("bitgreen-parachain"), impl_name: create_runtime_str!("bitgreen-parachain"), authoring_version: 1, - spec_version: 1101, // v1.1.1 + spec_version: 1105, // v1.1.5 impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -496,16 +493,19 @@ impl pallet_assets::Config for Runtime { type WeightInfo = (); } -impl pallet_membership::Config for Runtime { +parameter_types! { + pub const KYCPalletId: PalletId = PalletId(*b"bitg/kyc"); +} + +impl pallet_kyc::Config for Runtime { type AddOrigin = EnsureRoot; type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type PalletId = KYCPalletId; type MaxMembers = ConstU32<100_000>; type MembershipChanged = (); type MembershipInitialized = (); - type PrimeOrigin = EnsureRoot; - type RemoveOrigin = EnsureRoot; - type ResetOrigin = EnsureRoot; - type SwapOrigin = EnsureRoot; + type MaxAuthorizedAccountCount = ConstU32<100>; type WeightInfo = (); } @@ -533,7 +533,7 @@ impl pallet_carbon_credits::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ForceOrigin = EnsureRoot; type ItemId = u32; - type KYCProvider = KYCMembership; + type KYCProvider = KYC; type MarketplaceEscrow = MarketplaceEscrowAccount; type MaxAuthorizedAccountCount = MaxAuthorizedAccountCount; type MaxDocumentCount = MaxDocumentCount; @@ -657,6 +657,7 @@ impl pallet_vesting_contract::Config for Runtime { type ForceOrigin = EnsureRoot; type MaxContractInputLength = MaxContractInputLength; type PalletId = VestingContractPalletId; + type MaxAuthorizedAccountCount = MaxAuthorizedAccountCount; type WeightInfo = (); } @@ -728,11 +729,16 @@ impl pallet_multisig::Config for Runtime { // TODO: test limits are safe parameter_types! { pub const DexPalletId: PalletId = PalletId(*b"bitg/dex"); - pub const StableCurrencyId: primitives::CurrencyId = primitives::CurrencyId::USDT; pub const MinUnitsToCreateSellOrder : u32 = 100; pub const MinPricePerUnit : u32 = 1; pub const MaxPaymentFee : Percent = Percent::from_percent(10); pub const MaxPurchaseFee : Balance = 10 * UNIT; + #[derive(Clone, scale_info::TypeInfo)] + pub const MaxValidators : u32 = 10; + #[derive(Clone, scale_info::TypeInfo)] + pub const MaxTxHashLen : u32 = 100; + #[derive(Clone, scale_info::TypeInfo)] + pub const BuyOrderExpiryTime : u32 = 10; } impl pallet_dex::Config for Runtime { @@ -741,10 +747,13 @@ impl pallet_dex::Config for Runtime { type Currency = Tokens; type CurrencyBalance = u128; type AssetBalance = u128; - type StableCurrencyId = StableCurrencyId; type PalletId = DexPalletId; type AssetValidator = CarbonCredits; type MinPricePerUnit = MinPricePerUnit; + type MaxValidators = MaxValidators; + type MaxTxHashLen = MaxTxHashLen; + type KYCProvider = KYC; + type BuyOrderExpiryTime = BuyOrderExpiryTime; type MinUnitsToCreateSellOrder = MinUnitsToCreateSellOrder; type ForceOrigin = EnsureRoot; type MaxPaymentFee = MaxPaymentFee; @@ -949,7 +958,6 @@ construct_runtime!( Tokens: orml_tokens::{Pallet, Call, Storage, Event, Config} = 41, // Bitgreen pallets - KYCMembership: pallet_membership::{Pallet, Call, Storage, Config, Event} = 50, Sudo: pallet_sudo::{Pallet, Call, Storage, Config, Event} = 51, Assets: pallet_assets::{Pallet, Call, Storage, Event} = 52, Uniques: pallet_uniques::{Pallet, Call, Storage, Event} = 53, @@ -960,6 +968,7 @@ construct_runtime!( Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 58, Multisig: pallet_multisig::{Pallet, Call, Storage, Event} = 59, Dex: pallet_dex::{Pallet, Call, Storage, Event} = 60, + KYC: pallet_kyc::{Pallet, Call, Storage, Config, Event} = 66, // Utility pallets Utility: pallet_utility::{Pallet, Call, Event} = 61, diff --git a/runtime/rococo/Cargo.toml b/runtime/rococo/Cargo.toml index 8258e19b..191d40ee 100644 --- a/runtime/rococo/Cargo.toml +++ b/runtime/rococo/Cargo.toml @@ -99,6 +99,7 @@ pallet-carbon-credits-pool = { default-features = false, version = '0.0.1', path pallet-parachain-staking = { default-features = false, version = '0.0.1', path = "../../pallets/parachain-staking" } pallet-transaction-pause = { default-features = false, version = '0.0.1', path = "../../pallets/transaction-pause" } pallet-vesting-contract = { default-features = false, version = '0.0.1', path = "../../pallets/vesting-contract" } +pallet-kyc = { default-features = false, version = '0.0.1', path = "../../pallets/kyc" } pallet-dex = { default-features = false, path = "../../pallets/dex" } primitives = { package = "bitgreen-primitives", path = "../../primitives", default-features = false } @@ -154,6 +155,7 @@ std = [ "xcm/std", "pallet-uniques/std", "pallet-membership/std", + "pallet-kyc/std", "pallet-treasury/std", "pallet-contracts/std", "pallet-contracts-primitives/std", diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 2841999c..3eb07423 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -111,10 +111,7 @@ pub type Executive = frame_executive::Executive< Runtime, AllPalletsWithSystem, // Migrations - ( - pallet_parachain_staking::migration::v2::MigrateToV2, - pallet_carbon_credits::migration::v1::MigrateToV1, - ), + (pallet_parachain_staking::migration::v3::MigrateToV3,), >; pub type NegativeImbalance = as Currency< @@ -177,7 +174,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("bitgreen-rococo"), impl_name: create_runtime_str!("bitgreen-rococo"), authoring_version: 1, - spec_version: 1101, // v1.1.1 + spec_version: 1105, // v1.1.5 impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -495,19 +492,21 @@ impl pallet_assets::Config for Runtime { type WeightInfo = (); } -impl pallet_membership::Config for Runtime { +parameter_types! { + pub const KYCPalletId: PalletId = PalletId(*b"bitg/kyc"); +} + +impl pallet_kyc::Config for Runtime { type AddOrigin = EnsureRoot; type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type PalletId = KYCPalletId; type MaxMembers = ConstU32<100_000>; type MembershipChanged = (); type MembershipInitialized = (); - type PrimeOrigin = EnsureRoot; - type RemoveOrigin = EnsureRoot; - type ResetOrigin = EnsureRoot; - type SwapOrigin = EnsureRoot; + type MaxAuthorizedAccountCount = ConstU32<100>; type WeightInfo = (); } - parameter_types! { pub MarketplaceEscrowAccount : AccountId = PalletId(*b"bitg/mkp").into_account_truncating(); pub const CarbonCreditsPalletId: PalletId = PalletId(*b"bitg/vcu"); @@ -532,7 +531,7 @@ impl pallet_carbon_credits::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ForceOrigin = EnsureRoot; type ItemId = u32; - type KYCProvider = KYCMembership; + type KYCProvider = KYC; type MarketplaceEscrow = MarketplaceEscrowAccount; type MaxAuthorizedAccountCount = MaxAuthorizedAccountCount; type MaxDocumentCount = MaxDocumentCount; @@ -656,6 +655,32 @@ impl pallet_vesting_contract::Config for Runtime { type ForceOrigin = EnsureRoot; type MaxContractInputLength = MaxContractInputLength; type PalletId = VestingContractPalletId; + type MaxAuthorizedAccountCount = MaxAuthorizedAccountCount; + type WeightInfo = (); +} + +parameter_types! { + // Minimum 4 CENTS/byte + pub const BasicDeposit: Balance = deposit(1, 258); + pub const FieldDeposit: Balance = deposit(0, 66); + pub const SubAccountDeposit: Balance = deposit(1, 53); + pub const MaxSubAccounts: u32 = 100; + pub const MaxAdditionalFields: u32 = 100; + pub const MaxRegistrars: u32 = 20; +} + +impl pallet_identity::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type BasicDeposit = BasicDeposit; + type FieldDeposit = FieldDeposit; + type SubAccountDeposit = SubAccountDeposit; + type MaxSubAccounts = MaxSubAccounts; + type MaxAdditionalFields = MaxAdditionalFields; + type MaxRegistrars = MaxRegistrars; + type Slashed = Treasury; + type ForceOrigin = EnsureRoot; + type RegistrarOrigin = EnsureRoot; type WeightInfo = (); } @@ -752,11 +777,16 @@ impl pallet_multisig::Config for Runtime { // TODO: test limits are safe parameter_types! { pub const DexPalletId: PalletId = PalletId(*b"bitg/dex"); - pub const StableCurrencyId: primitives::CurrencyId = primitives::CurrencyId::USDT; pub const MinUnitsToCreateSellOrder : u32 = 100; pub const MinPricePerUnit : u32 = 1; pub const MaxPaymentFee : Percent = Percent::from_percent(10); pub const MaxPurchaseFee : Balance = 10 * UNIT; + #[derive(Clone, scale_info::TypeInfo)] + pub const MaxValidators : u32 = 10; + #[derive(Clone, scale_info::TypeInfo)] + pub const MaxTxHashLen : u32 = 100; + #[derive(Clone, scale_info::TypeInfo)] + pub const BuyOrderExpiryTime : u32 = 10; } impl pallet_dex::Config for Runtime { @@ -765,12 +795,15 @@ impl pallet_dex::Config for Runtime { type Currency = Tokens; type CurrencyBalance = u128; type AssetBalance = u128; - type StableCurrencyId = StableCurrencyId; type PalletId = DexPalletId; type AssetValidator = CarbonCredits; type MinPricePerUnit = MinPricePerUnit; + type KYCProvider = KYC; type MinUnitsToCreateSellOrder = MinUnitsToCreateSellOrder; type ForceOrigin = EnsureRoot; + type MaxValidators = MaxValidators; + type MaxTxHashLen = MaxTxHashLen; + type BuyOrderExpiryTime = BuyOrderExpiryTime; type MaxPaymentFee = MaxPaymentFee; type MaxPurchaseFee = MaxPurchaseFee; type WeightInfo = (); @@ -947,7 +980,6 @@ construct_runtime!( Tokens: orml_tokens::{Pallet, Call, Storage, Event, Config} = 41, // Bitgreen pallets - KYCMembership: pallet_membership::{Pallet, Call, Storage, Config, Event} = 50, Sudo: pallet_sudo::{Pallet, Call, Storage, Config, Event} = 51, Assets: pallet_assets::{Pallet, Call, Storage, Event} = 52, Uniques: pallet_uniques::{Pallet, Call, Storage, Event} = 53, @@ -958,6 +990,7 @@ construct_runtime!( Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 58, Multisig: pallet_multisig::{Pallet, Call, Storage, Event} = 59, Dex: pallet_dex::{Pallet, Call, Storage, Event} = 60, + KYC: pallet_kyc::{Pallet, Call, Storage, Config, Event} = 66, // Utility pallets Utility: pallet_utility::{Pallet, Call, Event} = 61, diff --git a/scripts/polkadot-launch/config.json b/scripts/polkadot-launch/config.json index 87f66c8d..27c7a100 100644 --- a/scripts/polkadot-launch/config.json +++ b/scripts/polkadot-launch/config.json @@ -1,6 +1,6 @@ { "relaychain": { - "bin": "../../../polkadot/target/release/polkadot", + "bin": "../../../../polkadot/target/release/polkadot", "chain": "rococo-local", "nodes": [ { @@ -17,8 +17,8 @@ }, "parachains": [ { - "bin": "../target/release/bitgreen-parachain", - "chain": "rococo-local", + "bin": "../../target/release/bitgreen-parachain", + "chain": "dev", "nodes": [ { "wsPort": 9946,