diff --git a/pallets/funding/src/instantiator.rs b/pallets/funding/src/instantiator.rs deleted file mode 100644 index 2163d3fb3..000000000 --- a/pallets/funding/src/instantiator.rs +++ /dev/null @@ -1,3279 +0,0 @@ -// Polimec Blockchain – https://www.polimec.org/ -// Copyright (C) Polimec 2022. All rights reserved. - -// The Polimec Blockchain is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// The Polimec Blockchain is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::{ - traits::{BondingRequirementCalculation, ProvideAssetPrice}, - *, -}; -use frame_support::{ - pallet_prelude::*, - traits::{ - fungible::{Inspect as FungibleInspect, InspectHold as FungibleInspectHold, Mutate as FungibleMutate}, - fungibles::{ - metadata::Inspect as MetadataInspect, roles::Inspect as RolesInspect, Inspect as FungiblesInspect, - Mutate as FungiblesMutate, - }, - AccountTouch, Get, OnFinalize, OnIdle, OnInitialize, - }, - weights::Weight, - Parameter, -}; -use frame_system::pallet_prelude::BlockNumberFor; -use itertools::Itertools; -use parity_scale_codec::Decode; -use polimec_common::{credentials::InvestorType, migration_types::MigrationOrigin}; -#[cfg(any(test, feature = "std", feature = "runtime-benchmarks"))] -use polimec_common_test_utils::generate_did_from_account; -use sp_arithmetic::{ - traits::{SaturatedConversion, Saturating, Zero}, - FixedPointNumber, Percent, Perquintill, -}; -use sp_runtime::{ - traits::{Convert, Member, One}, - DispatchError, -}; -use sp_std::{ - cell::RefCell, - collections::{btree_map::*, btree_set::*}, - iter::zip, - marker::PhantomData, -}; - -pub type RuntimeOriginOf = ::RuntimeOrigin; -pub struct BoxToFunction(pub Box); -impl Default for BoxToFunction { - fn default() -> Self { - BoxToFunction(Box::new(|| ())) - } -} - -#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "std", - serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) -)] -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] -pub struct TestProjectParams { - pub expected_state: ProjectStatus, - pub metadata: ProjectMetadataOf, - pub issuer: AccountIdOf, - pub evaluations: Vec>, - pub bids: Vec>, - pub community_contributions: Vec>, - pub remainder_contributions: Vec>, -} - -#[cfg(feature = "std")] -type OptionalExternalities = Option>; - -#[cfg(not(feature = "std"))] -type OptionalExternalities = Option<()>; - -pub struct Instantiator< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, -> { - ext: OptionalExternalities, - nonce: RefCell, - _marker: PhantomData<(T, AllPalletsWithoutSystem, RuntimeEvent)>, -} - -// general chain interactions -impl< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - > Instantiator -{ - pub fn new(ext: OptionalExternalities) -> Self { - Self { ext, nonce: RefCell::new(0u64), _marker: PhantomData } - } - - pub fn set_ext(&mut self, ext: OptionalExternalities) { - self.ext = ext; - } - - pub fn execute(&mut self, execution: impl FnOnce() -> R) -> R { - #[cfg(feature = "std")] - if let Some(ext) = &self.ext { - return ext.borrow_mut().execute_with(execution); - } - execution() - } - - pub fn get_new_nonce(&self) -> u64 { - let nonce = *self.nonce.borrow_mut(); - self.nonce.replace(nonce + 1); - nonce - } - - pub fn get_free_plmc_balances_for(&mut self, user_keys: Vec>) -> Vec> { - self.execute(|| { - let mut balances: Vec> = Vec::new(); - for account in user_keys { - let plmc_amount = ::NativeCurrency::balance(&account); - balances.push(UserToPLMCBalance { account, plmc_amount }); - } - balances.sort_by_key(|a| a.account.clone()); - balances - }) - } - - pub fn get_reserved_plmc_balances_for( - &mut self, - user_keys: Vec>, - lock_type: ::RuntimeHoldReason, - ) -> Vec> { - self.execute(|| { - let mut balances: Vec> = Vec::new(); - for account in user_keys { - let plmc_amount = ::NativeCurrency::balance_on_hold(&lock_type, &account); - balances.push(UserToPLMCBalance { account, plmc_amount }); - } - balances.sort_by(|a, b| a.account.cmp(&b.account)); - balances - }) - } - - pub fn get_free_foreign_asset_balances_for( - &mut self, - asset_id: AssetIdOf, - user_keys: Vec>, - ) -> Vec> { - self.execute(|| { - let mut balances: Vec> = Vec::new(); - for account in user_keys { - let asset_amount = ::FundingCurrency::balance(asset_id, &account); - balances.push(UserToForeignAssets { account, asset_amount, asset_id }); - } - balances.sort_by(|a, b| a.account.cmp(&b.account)); - balances - }) - } - - pub fn get_ct_asset_balances_for( - &mut self, - project_id: ProjectId, - user_keys: Vec>, - ) -> Vec> { - self.execute(|| { - let mut balances: Vec> = Vec::new(); - for account in user_keys { - let asset_amount = ::ContributionTokenCurrency::balance(project_id, &account); - balances.push(asset_amount); - } - balances - }) - } - - pub fn get_all_free_plmc_balances(&mut self) -> Vec> { - let user_keys = self.execute(|| frame_system::Account::::iter_keys().collect()); - self.get_free_plmc_balances_for(user_keys) - } - - pub fn get_all_reserved_plmc_balances( - &mut self, - reserve_type: ::RuntimeHoldReason, - ) -> Vec> { - let user_keys = self.execute(|| frame_system::Account::::iter_keys().collect()); - self.get_reserved_plmc_balances_for(user_keys, reserve_type) - } - - pub fn get_all_free_foreign_asset_balances(&mut self, asset_id: AssetIdOf) -> Vec> { - let user_keys = self.execute(|| frame_system::Account::::iter_keys().collect()); - self.get_free_foreign_asset_balances_for(asset_id, user_keys) - } - - pub fn get_plmc_total_supply(&mut self) -> BalanceOf { - self.execute(::NativeCurrency::total_issuance) - } - - pub fn do_reserved_plmc_assertions( - &mut self, - correct_funds: Vec>, - reserve_type: ::RuntimeHoldReason, - ) { - for UserToPLMCBalance { account, plmc_amount } in correct_funds { - self.execute(|| { - let reserved = ::NativeCurrency::balance_on_hold(&reserve_type, &account); - assert_eq!(reserved, plmc_amount, "account has unexpected reserved plmc balance"); - }); - } - } - - pub fn mint_plmc_to(&mut self, mapping: Vec>) { - self.execute(|| { - for UserToPLMCBalance { account, plmc_amount } in mapping { - ::NativeCurrency::mint_into(&account, plmc_amount).expect("Minting should work"); - } - }); - } - - pub fn mint_foreign_asset_to(&mut self, mapping: Vec>) { - self.execute(|| { - for UserToForeignAssets { account, asset_amount, asset_id } in mapping { - ::FundingCurrency::mint_into(asset_id, &account, asset_amount) - .expect("Minting should work"); - } - }); - } - - pub fn current_block(&mut self) -> BlockNumberFor { - self.execute(|| frame_system::Pallet::::block_number()) - } - - pub fn advance_time(&mut self, amount: BlockNumberFor) -> Result<(), DispatchError> { - self.execute(|| { - for _block in 0u32..amount.saturated_into() { - let mut current_block = frame_system::Pallet::::block_number(); - - >>::on_finalize(current_block); - as OnFinalize>>::on_finalize(current_block); - - >>::on_idle(current_block, Weight::MAX); - as OnIdle>>::on_idle(current_block, Weight::MAX); - - current_block += One::one(); - frame_system::Pallet::::set_block_number(current_block); - - as OnInitialize>>::on_initialize(current_block); - >>::on_initialize(current_block); - } - Ok(()) - }) - } - - pub fn do_free_plmc_assertions(&mut self, correct_funds: Vec>) { - for UserToPLMCBalance { account, plmc_amount } in correct_funds { - self.execute(|| { - let free = ::NativeCurrency::balance(&account); - assert_eq!(free, plmc_amount, "account has unexpected free plmc balance"); - }); - } - } - - pub fn do_free_foreign_asset_assertions(&mut self, correct_funds: Vec>) { - for UserToForeignAssets { account, asset_amount, asset_id } in correct_funds { - self.execute(|| { - let real_amount = ::FundingCurrency::balance(asset_id, &account); - assert_eq!(asset_amount, real_amount, "Wrong foreign asset balance expected for user {:?}", account); - }); - } - } - - pub fn do_bid_transferred_foreign_asset_assertions( - &mut self, - correct_funds: Vec>, - project_id: ProjectId, - ) { - for UserToForeignAssets { account, asset_amount, .. } in correct_funds { - self.execute(|| { - // total amount of contributions for this user for this project stored in the mapping - let contribution_total: ::Balance = - Bids::::iter_prefix_values((project_id, account.clone())) - .map(|c| c.funding_asset_amount_locked) - .fold(Zero::zero(), |a, b| a + b); - assert_eq!( - contribution_total, asset_amount, - "Wrong funding balance expected for stored auction info on user {:?}", - account - ); - }); - } - } - - // Check if a Contribution storage item exists for the given funding asset transfer - pub fn do_contribution_transferred_foreign_asset_assertions( - &mut self, - correct_funds: Vec>, - project_id: ProjectId, - ) { - for UserToForeignAssets { account, asset_amount, .. } in correct_funds { - self.execute(|| { - Contributions::::iter_prefix_values((project_id, account.clone())) - .find(|c| c.funding_asset_amount == asset_amount) - .expect("Contribution not found in storage"); - }); - } - } -} - -// assertions -impl< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - > Instantiator -{ - pub fn test_ct_created_for(&mut self, project_id: ProjectId) { - self.execute(|| { - let metadata = ProjectsMetadata::::get(project_id).unwrap(); - assert_eq!( - ::ContributionTokenCurrency::name(project_id), - metadata.token_information.name.to_vec() - ); - let escrow_account = Pallet::::fund_account_id(project_id); - - assert_eq!(::ContributionTokenCurrency::admin(project_id).unwrap(), escrow_account); - }); - } - - pub fn test_ct_not_created_for(&mut self, project_id: ProjectId) { - self.execute(|| { - assert!( - !::ContributionTokenCurrency::asset_exists(project_id), - "Asset shouldn't exist, since funding failed" - ); - }); - } - - pub fn creation_assertions( - &mut self, - project_id: ProjectId, - expected_metadata: ProjectMetadataOf, - creation_start_block: BlockNumberFor, - ) { - let metadata = self.get_project_metadata(project_id); - let details = self.get_project_details(project_id); - let expected_details = ProjectDetailsOf:: { - issuer_account: self.get_issuer(project_id), - issuer_did: generate_did_from_account(self.get_issuer(project_id)), - is_frozen: false, - weighted_average_price: None, - status: ProjectStatus::Application, - phase_transition_points: PhaseTransitionPoints { - application: BlockNumberPair { start: Some(creation_start_block), end: None }, - ..Default::default() - }, - fundraising_target: expected_metadata - .minimum_price - .checked_mul_int(expected_metadata.total_allocation_size) - .unwrap(), - remaining_contribution_tokens: expected_metadata.total_allocation_size, - funding_amount_reached: BalanceOf::::zero(), - evaluation_round_info: EvaluationRoundInfoOf:: { - total_bonded_usd: Zero::zero(), - total_bonded_plmc: Zero::zero(), - evaluators_outcome: EvaluatorsOutcome::Unchanged, - }, - funding_end_block: None, - parachain_id: None, - migration_readiness_check: None, - hrmp_channel_status: HRMPChannelStatus { - project_to_polimec: crate::ChannelStatus::Closed, - polimec_to_project: crate::ChannelStatus::Closed, - }, - }; - assert_eq!(metadata, expected_metadata); - assert_eq!(details, expected_details); - } - - pub fn evaluation_assertions( - &mut self, - project_id: ProjectId, - expected_free_plmc_balances: Vec>, - expected_reserved_plmc_balances: Vec>, - total_plmc_supply: BalanceOf, - ) { - // just in case we forgot to merge accounts: - let expected_free_plmc_balances = - Self::generic_map_operation(vec![expected_free_plmc_balances], MergeOperation::Add); - let expected_reserved_plmc_balances = - Self::generic_map_operation(vec![expected_reserved_plmc_balances], MergeOperation::Add); - - let project_details = self.get_project_details(project_id); - - assert_eq!(project_details.status, ProjectStatus::EvaluationRound); - assert_eq!(self.get_plmc_total_supply(), total_plmc_supply); - self.do_free_plmc_assertions(expected_free_plmc_balances); - self.do_reserved_plmc_assertions(expected_reserved_plmc_balances, HoldReason::Evaluation(project_id).into()); - } - - pub fn finalized_bids_assertions( - &mut self, - project_id: ProjectId, - bid_expectations: Vec>, - expected_ct_sold: BalanceOf, - ) { - let project_metadata = self.get_project_metadata(project_id); - let project_details = self.get_project_details(project_id); - let project_bids = self.execute(|| Bids::::iter_prefix_values((project_id,)).collect::>()); - assert!(project_details.weighted_average_price.is_some(), "Weighted average price should exist"); - - for filter in bid_expectations { - let _found_bid = project_bids.iter().find(|bid| filter.matches_bid(bid)).unwrap(); - } - - // Remaining CTs are updated - assert_eq!( - project_details.remaining_contribution_tokens, - project_metadata.total_allocation_size - expected_ct_sold, - "Remaining CTs are incorrect" - ); - } -} - -// calculations -impl< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - > Instantiator -{ - pub fn get_ed() -> BalanceOf { - T::ExistentialDeposit::get() - } - - pub fn get_ct_account_deposit() -> BalanceOf { - ::ContributionTokenCurrency::deposit_required(One::one()) - } - - pub fn calculate_evaluation_plmc_spent(evaluations: Vec>) -> Vec> { - let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); - let mut output = Vec::new(); - for eval in evaluations { - let usd_bond = eval.usd_amount; - let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); - output.push(UserToPLMCBalance::new(eval.account, plmc_bond)); - } - output - } - - pub fn get_actual_price_charged_for_bucketed_bids( - bids: &Vec>, - project_metadata: ProjectMetadataOf, - maybe_bucket: Option>, - ) -> Vec<(BidParams, PriceOf)> { - let mut output = Vec::new(); - let mut bucket = if let Some(bucket) = maybe_bucket { - bucket - } else { - Pallet::::create_bucket_from_metadata(&project_metadata).unwrap() - }; - for bid in bids { - let mut amount_to_bid = bid.amount; - while !amount_to_bid.is_zero() { - let bid_amount = if amount_to_bid <= bucket.amount_left { amount_to_bid } else { bucket.amount_left }; - output.push(( - BidParams { - bidder: bid.bidder.clone(), - amount: bid_amount, - multiplier: bid.multiplier, - asset: bid.asset, - }, - bucket.current_price, - )); - bucket.update(bid_amount); - amount_to_bid.saturating_reduce(bid_amount); - } - } - output - } - - pub fn calculate_auction_plmc_charged_with_given_price( - bids: &Vec>, - ct_price: PriceOf, - ) -> Vec> { - let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); - let mut output = Vec::new(); - for bid in bids { - let usd_ticket_size = ct_price.saturating_mul_int(bid.amount); - let usd_bond = bid.multiplier.calculate_bonding_requirement::(usd_ticket_size).unwrap(); - let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); - output.push(UserToPLMCBalance::new(bid.bidder.clone(), plmc_bond)); - } - output - } - - // Make sure you give it all the bids made for the project. It doesn't require a ct_price, since it will simulate the bucket prices itself - pub fn calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - bids: &Vec>, - project_metadata: ProjectMetadataOf, - maybe_bucket: Option>, - ) -> Vec> { - let mut output = Vec::new(); - let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); - - for (bid, price) in Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata, maybe_bucket) { - let usd_ticket_size = price.saturating_mul_int(bid.amount); - let usd_bond = bid.multiplier.calculate_bonding_requirement::(usd_ticket_size).unwrap(); - let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); - output.push(UserToPLMCBalance::::new(bid.bidder.clone(), plmc_bond)); - } - - output.merge_accounts(MergeOperation::Add) - } - - // WARNING: Only put bids that you are sure will be done before the random end of the closing auction - pub fn calculate_auction_plmc_returned_from_all_bids_made( - // bids in the order they were made - bids: &Vec>, - project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, - ) -> Vec> { - let mut output = Vec::new(); - let charged_bids = Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata.clone(), None); - let grouped_by_price_bids = charged_bids.clone().into_iter().group_by(|&(_, price)| price); - let mut grouped_by_price_bids: Vec<(PriceOf, Vec>)> = grouped_by_price_bids - .into_iter() - .map(|(key, group)| (key, group.map(|(bid, _price_)| bid).collect())) - .collect(); - grouped_by_price_bids.reverse(); - - let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); - let mut remaining_cts = - project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; - - for (price_charged, bids) in grouped_by_price_bids { - for bid in bids { - let charged_usd_ticket_size = price_charged.saturating_mul_int(bid.amount); - let charged_usd_bond = - bid.multiplier.calculate_bonding_requirement::(charged_usd_ticket_size).unwrap(); - let charged_plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(charged_usd_bond); - - if remaining_cts <= Zero::zero() { - output.push(UserToPLMCBalance::new(bid.bidder, charged_plmc_bond)); - continue - } - - let bought_cts = if remaining_cts < bid.amount { remaining_cts } else { bid.amount }; - remaining_cts = remaining_cts.saturating_sub(bought_cts); - - let final_price = - if weighted_average_price > price_charged { price_charged } else { weighted_average_price }; - - let actual_usd_ticket_size = final_price.saturating_mul_int(bought_cts); - let actual_usd_bond = - bid.multiplier.calculate_bonding_requirement::(actual_usd_ticket_size).unwrap(); - let actual_plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(actual_usd_bond); - - let returned_plmc_bond = charged_plmc_bond - actual_plmc_bond; - - output.push(UserToPLMCBalance::::new(bid.bidder, returned_plmc_bond)); - } - } - - output.merge_accounts(MergeOperation::Add) - } - - pub fn calculate_auction_plmc_spent_post_wap( - bids: &Vec>, - project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, - ) -> Vec> { - let plmc_charged = Self::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - bids, - project_metadata.clone(), - None, - ); - let plmc_returned = Self::calculate_auction_plmc_returned_from_all_bids_made( - bids, - project_metadata.clone(), - weighted_average_price, - ); - - plmc_charged.subtract_accounts(plmc_returned) - } - - pub fn calculate_auction_funding_asset_charged_with_given_price( - bids: &Vec>, - ct_price: PriceOf, - ) -> Vec> { - let mut output = Vec::new(); - for bid in bids { - let asset_price = T::PriceProvider::get_price(bid.asset.to_assethub_id()).unwrap(); - let usd_ticket_size = ct_price.saturating_mul_int(bid.amount); - let funding_asset_spent = asset_price.reciprocal().unwrap().saturating_mul_int(usd_ticket_size); - output.push(UserToForeignAssets::new(bid.bidder.clone(), funding_asset_spent, bid.asset.to_assethub_id())); - } - output - } - - // Make sure you give it all the bids made for the project. It doesn't require a ct_price, since it will simulate the bucket prices itself - pub fn calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - bids: &Vec>, - project_metadata: ProjectMetadataOf, - maybe_bucket: Option>, - ) -> Vec> { - let mut output = Vec::new(); - - for (bid, price) in Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata, maybe_bucket) { - let asset_price = T::PriceProvider::get_price(bid.asset.to_assethub_id()).unwrap(); - let usd_ticket_size = price.saturating_mul_int(bid.amount); - let funding_asset_spent = asset_price.reciprocal().unwrap().saturating_mul_int(usd_ticket_size); - output.push(UserToForeignAssets::::new( - bid.bidder.clone(), - funding_asset_spent, - bid.asset.to_assethub_id(), - )); - } - - output.merge_accounts(MergeOperation::Add) - } - - // WARNING: Only put bids that you are sure will be done before the random end of the closing auction - pub fn calculate_auction_funding_asset_returned_from_all_bids_made( - // bids in the order they were made - bids: &Vec>, - project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, - ) -> Vec> { - let mut output = Vec::new(); - let charged_bids = Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata.clone(), None); - let grouped_by_price_bids = charged_bids.clone().into_iter().group_by(|&(_, price)| price); - let mut grouped_by_price_bids: Vec<(PriceOf, Vec>)> = grouped_by_price_bids - .into_iter() - .map(|(key, group)| (key, group.map(|(bid, _price)| bid).collect())) - .collect(); - grouped_by_price_bids.reverse(); - - let mut remaining_cts = - project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; - - for (price_charged, bids) in grouped_by_price_bids { - for bid in bids { - let funding_asset_price = T::PriceProvider::get_price(bid.asset.to_assethub_id()).unwrap(); - - let charged_usd_ticket_size = price_charged.saturating_mul_int(bid.amount); - let charged_usd_bond = - bid.multiplier.calculate_bonding_requirement::(charged_usd_ticket_size).unwrap(); - let charged_funding_asset = - funding_asset_price.reciprocal().unwrap().saturating_mul_int(charged_usd_bond); - - if remaining_cts <= Zero::zero() { - output.push(UserToForeignAssets::new( - bid.bidder, - charged_funding_asset, - bid.asset.to_assethub_id(), - )); - continue - } - - let bought_cts = if remaining_cts < bid.amount { remaining_cts } else { bid.amount }; - remaining_cts = remaining_cts.saturating_sub(bought_cts); - - let final_price = - if weighted_average_price > price_charged { price_charged } else { weighted_average_price }; - - let actual_usd_ticket_size = final_price.saturating_mul_int(bought_cts); - let actual_usd_bond = - bid.multiplier.calculate_bonding_requirement::(actual_usd_ticket_size).unwrap(); - let actual_funding_asset_spent = - funding_asset_price.reciprocal().unwrap().saturating_mul_int(actual_usd_bond); - - let returned_foreign_asset = charged_funding_asset - actual_funding_asset_spent; - - output.push(UserToForeignAssets::::new( - bid.bidder, - returned_foreign_asset, - bid.asset.to_assethub_id(), - )); - } - } - - output.merge_accounts(MergeOperation::Add) - } - - pub fn calculate_auction_funding_asset_spent_post_wap( - bids: &Vec>, - project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, - ) -> Vec> { - let funding_asset_charged = Self::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - bids, - project_metadata.clone(), - None, - ); - let funding_asset_returned = Self::calculate_auction_funding_asset_returned_from_all_bids_made( - bids, - project_metadata.clone(), - weighted_average_price, - ); - - funding_asset_charged.subtract_accounts(funding_asset_returned) - } - - /// Filters the bids that would be rejected after the auction ends. - pub fn filter_bids_after_auction(bids: Vec>, total_cts: BalanceOf) -> Vec> { - let mut filtered_bids: Vec> = Vec::new(); - let sorted_bids = bids; - let mut total_cts_left = total_cts; - for bid in sorted_bids { - if total_cts_left >= bid.amount { - total_cts_left.saturating_reduce(bid.amount); - filtered_bids.push(bid); - } else if !total_cts_left.is_zero() { - filtered_bids.push(BidParams { - bidder: bid.bidder.clone(), - amount: total_cts_left, - multiplier: bid.multiplier, - asset: bid.asset, - }); - total_cts_left = Zero::zero(); - } - } - filtered_bids - } - - pub fn calculate_contributed_plmc_spent( - contributions: Vec>, - token_usd_price: PriceOf, - ) -> Vec> { - let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); - let mut output = Vec::new(); - for cont in contributions { - let usd_ticket_size = token_usd_price.saturating_mul_int(cont.amount); - let usd_bond = cont.multiplier.calculate_bonding_requirement::(usd_ticket_size).unwrap(); - let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); - output.push(UserToPLMCBalance::new(cont.contributor, plmc_bond)); - } - output - } - - pub fn calculate_total_plmc_locked_from_evaluations_and_remainder_contributions( - evaluations: Vec>, - contributions: Vec>, - price: PriceOf, - slashed: bool, - ) -> Vec> { - let evaluation_locked_plmc_amounts = Self::calculate_evaluation_plmc_spent(evaluations); - // how much new plmc would be locked without considering evaluation bonds - let theoretical_contribution_locked_plmc_amounts = Self::calculate_contributed_plmc_spent(contributions, price); - - let slash_percentage = ::EvaluatorSlash::get(); - let slashable_min_deposits = evaluation_locked_plmc_amounts - .iter() - .map(|UserToPLMCBalance { account, plmc_amount }| UserToPLMCBalance { - account: account.clone(), - plmc_amount: slash_percentage * *plmc_amount, - }) - .collect::>(); - let available_evaluation_locked_plmc_for_lock_transfer = Self::generic_map_operation( - vec![evaluation_locked_plmc_amounts.clone(), slashable_min_deposits.clone()], - MergeOperation::Subtract, - ); - - // how much new plmc was actually locked, considering already evaluation bonds used - // first. - let actual_contribution_locked_plmc_amounts = Self::generic_map_operation( - vec![theoretical_contribution_locked_plmc_amounts, available_evaluation_locked_plmc_for_lock_transfer], - MergeOperation::Subtract, - ); - let mut result = Self::generic_map_operation( - vec![evaluation_locked_plmc_amounts, actual_contribution_locked_plmc_amounts], - MergeOperation::Add, - ); - - if slashed { - result = Self::generic_map_operation(vec![result, slashable_min_deposits], MergeOperation::Subtract); - } - - result - } - - pub fn calculate_contributed_funding_asset_spent( - contributions: Vec>, - token_usd_price: PriceOf, - ) -> Vec> { - let mut output = Vec::new(); - for cont in contributions { - let asset_price = T::PriceProvider::get_price(cont.asset.to_assethub_id()).unwrap(); - let usd_ticket_size = token_usd_price.saturating_mul_int(cont.amount); - let funding_asset_spent = asset_price.reciprocal().unwrap().saturating_mul_int(usd_ticket_size); - output.push(UserToForeignAssets::new(cont.contributor, funding_asset_spent, cont.asset.to_assethub_id())); - } - output - } - - pub fn generic_map_merge_reduce( - mappings: Vec>, - key_extractor: impl Fn(&M) -> K, - initial_state: S, - merge_reduce: impl Fn(&M, S) -> S, - ) -> Vec<(K, S)> { - let mut output = BTreeMap::new(); - for mut map in mappings { - for item in map.drain(..) { - let key = key_extractor(&item); - let new_state = merge_reduce(&item, output.get(&key).cloned().unwrap_or(initial_state.clone())); - output.insert(key, new_state); - } - } - output.into_iter().collect() - } - - /// Merge the given mappings into one mapping, where the values are merged using the given - /// merge operation. - /// - /// In case of the `Add` operation, all values are Unioned, and duplicate accounts are - /// added together. - /// In case of the `Subtract` operation, all values of the first mapping are subtracted by - /// the values of the other mappings. Accounts in the other mappings that are not present - /// in the first mapping are ignored. - /// - /// # Pseudocode Example - /// List1: [(A, 10), (B, 5), (C, 5)] - /// List2: [(A, 5), (B, 5), (D, 5)] - /// - /// Add: [(A, 15), (B, 10), (C, 5), (D, 5)] - /// Subtract: [(A, 5), (B, 0), (C, 5)] - pub fn generic_map_operation< - N: AccountMerge + Extend<::Inner> + IntoIterator::Inner>, - >( - mut mappings: Vec, - ops: MergeOperation, - ) -> N { - let mut output = mappings.swap_remove(0); - output = output.merge_accounts(MergeOperation::Add); - for map in mappings { - match ops { - MergeOperation::Add => output.extend(map), - MergeOperation::Subtract => output = output.subtract_accounts(map), - } - } - output.merge_accounts(ops) - } - - pub fn sum_balance_mappings(mut mappings: Vec>>) -> BalanceOf { - let mut output = mappings - .swap_remove(0) - .into_iter() - .map(|user_to_plmc| user_to_plmc.plmc_amount) - .fold(Zero::zero(), |a, b| a + b); - for map in mappings { - output += map.into_iter().map(|user_to_plmc| user_to_plmc.plmc_amount).fold(Zero::zero(), |a, b| a + b); - } - output - } - - pub fn sum_foreign_mappings(mut mappings: Vec>>) -> BalanceOf { - let mut output = mappings - .swap_remove(0) - .into_iter() - .map(|user_to_asset| user_to_asset.asset_amount) - .fold(Zero::zero(), |a, b| a + b); - for map in mappings { - output += map.into_iter().map(|user_to_asset| user_to_asset.asset_amount).fold(Zero::zero(), |a, b| a + b); - } - output - } - - pub fn generate_successful_evaluations( - project_metadata: ProjectMetadataOf, - evaluators: Vec>, - weights: Vec, - ) -> Vec> { - let funding_target = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); - let evaluation_success_threshold = ::EvaluationSuccessThreshold::get(); // if we use just the threshold, then for big usd targets we lose the evaluation due to PLMC conversion errors in `evaluation_end` - let usd_threshold = evaluation_success_threshold * funding_target * 2u32.into(); - - zip(evaluators, weights) - .map(|(evaluator, weight)| { - let ticket_size = Percent::from_percent(weight) * usd_threshold; - (evaluator, ticket_size).into() - }) - .collect() - } - - pub fn generate_bids_from_total_usd( - usd_amount: BalanceOf, - min_price: PriceOf, - weights: Vec, - bidders: Vec>, - multipliers: Vec, - ) -> Vec> { - assert_eq!(weights.len(), bidders.len(), "Should have enough weights for all the bidders"); - - zip(zip(weights, bidders), multipliers) - .map(|((weight, bidder), multiplier)| { - let ticket_size = Percent::from_percent(weight) * usd_amount; - let token_amount = min_price.reciprocal().unwrap().saturating_mul_int(ticket_size); - - BidParams::new(bidder, token_amount, multiplier, AcceptedFundingAsset::USDT) - }) - .collect() - } - - pub fn generate_bids_from_total_ct_percent( - project_metadata: ProjectMetadataOf, - percent_funding: u8, - weights: Vec, - bidders: Vec>, - multipliers: Vec, - ) -> Vec> { - let total_allocation_size = project_metadata.total_allocation_size; - let total_ct_bid = Percent::from_percent(percent_funding) * total_allocation_size; - - assert_eq!(weights.len(), bidders.len(), "Should have enough weights for all the bidders"); - - zip(zip(weights, bidders), multipliers) - .map(|((weight, bidder), multiplier)| { - let token_amount = Percent::from_percent(weight) * total_ct_bid; - BidParams::new(bidder, token_amount, multiplier, AcceptedFundingAsset::USDT) - }) - .collect() - } - - pub fn generate_contributions_from_total_usd( - usd_amount: BalanceOf, - final_price: PriceOf, - weights: Vec, - contributors: Vec>, - multipliers: Vec, - ) -> Vec> { - zip(zip(weights, contributors), multipliers) - .map(|((weight, bidder), multiplier)| { - let ticket_size = Percent::from_percent(weight) * usd_amount; - let token_amount = final_price.reciprocal().unwrap().saturating_mul_int(ticket_size); - - ContributionParams::new(bidder, token_amount, multiplier, AcceptedFundingAsset::USDT) - }) - .collect() - } - - pub fn generate_contributions_from_total_ct_percent( - project_metadata: ProjectMetadataOf, - percent_funding: u8, - weights: Vec, - contributors: Vec>, - multipliers: Vec, - ) -> Vec> { - let total_allocation_size = project_metadata.total_allocation_size; - let total_ct_bought = Percent::from_percent(percent_funding) * total_allocation_size; - - assert_eq!(weights.len(), contributors.len(), "Should have enough weights for all the bidders"); - - zip(zip(weights, contributors), multipliers) - .map(|((weight, contributor), multiplier)| { - let token_amount = Percent::from_percent(weight) * total_ct_bought; - ContributionParams::new(contributor, token_amount, multiplier, AcceptedFundingAsset::USDT) - }) - .collect() - } - - pub fn slash_evaluator_balances(mut balances: Vec>) -> Vec> { - let slash_percentage = ::EvaluatorSlash::get(); - for UserToPLMCBalance { account: _acc, plmc_amount: balance } in balances.iter_mut() { - *balance -= slash_percentage * *balance; - } - balances - } - - pub fn calculate_total_reward_for_evaluation( - evaluation: EvaluationInfoOf, - reward_info: RewardInfoOf, - ) -> BalanceOf { - let early_reward_weight = - Perquintill::from_rational(evaluation.early_usd_amount, reward_info.early_evaluator_total_bonded_usd); - let normal_reward_weight = Perquintill::from_rational( - evaluation.late_usd_amount.saturating_add(evaluation.early_usd_amount), - reward_info.normal_evaluator_total_bonded_usd, - ); - let early_evaluators_rewards = early_reward_weight * reward_info.early_evaluator_reward_pot; - let normal_evaluators_rewards = normal_reward_weight * reward_info.normal_evaluator_reward_pot; - - early_evaluators_rewards.saturating_add(normal_evaluators_rewards) - } -} - -// project chain interactions -impl< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - > Instantiator -{ - pub fn get_issuer(&mut self, project_id: ProjectId) -> AccountIdOf { - self.execute(|| ProjectsDetails::::get(project_id).unwrap().issuer_account) - } - - pub fn get_project_metadata(&mut self, project_id: ProjectId) -> ProjectMetadataOf { - self.execute(|| ProjectsMetadata::::get(project_id).expect("Project metadata exists")) - } - - pub fn get_project_details(&mut self, project_id: ProjectId) -> ProjectDetailsOf { - self.execute(|| ProjectsDetails::::get(project_id).expect("Project details exists")) - } - - pub fn get_update_block(&mut self, project_id: ProjectId, update_type: &UpdateType) -> Option> { - self.execute(|| { - ProjectsToUpdate::::iter().find_map(|(block, update_tup)| { - if project_id == update_tup.0 && update_type == &update_tup.1 { - Some(block) - } else { - None - } - }) - }) - } - - pub fn create_new_project(&mut self, project_metadata: ProjectMetadataOf, issuer: AccountIdOf) -> ProjectId { - let now = self.current_block(); - // one ED for the issuer, one ED for the escrow account - self.mint_plmc_to(vec![UserToPLMCBalance::new(issuer.clone(), Self::get_ed() * 2u64.into())]); - - self.execute(|| { - crate::Pallet::::do_create_project( - &issuer, - project_metadata.clone(), - generate_did_from_account(issuer.clone()), - ) - .unwrap(); - let last_project_metadata = ProjectsMetadata::::iter().last().unwrap(); - log::trace!("Last project metadata: {:?}", last_project_metadata); - }); - - let created_project_id = self.execute(|| NextProjectId::::get().saturating_sub(One::one())); - self.creation_assertions(created_project_id, project_metadata, now); - created_project_id - } - - pub fn start_evaluation(&mut self, project_id: ProjectId, caller: AccountIdOf) -> Result<(), DispatchError> { - assert_eq!(self.get_project_details(project_id).status, ProjectStatus::Application); - self.execute(|| crate::Pallet::::do_start_evaluation(caller, project_id).unwrap()); - assert_eq!(self.get_project_details(project_id).status, ProjectStatus::EvaluationRound); - - Ok(()) - } - - pub fn create_evaluating_project( - &mut self, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - ) -> ProjectId { - let project_id = self.create_new_project(project_metadata, issuer.clone()); - self.start_evaluation(project_id, issuer).unwrap(); - project_id - } - - pub fn evaluate_for_users( - &mut self, - project_id: ProjectId, - bonds: Vec>, - ) -> DispatchResultWithPostInfo { - for UserToUSDBalance { account, usd_amount } in bonds { - self.execute(|| { - crate::Pallet::::do_evaluate( - &account.clone(), - project_id, - usd_amount, - generate_did_from_account(account), - InvestorType::Professional, - ) - })?; - } - Ok(().into()) - } - - pub fn start_auction(&mut self, project_id: ProjectId, caller: AccountIdOf) -> Result<(), DispatchError> { - let project_details = self.get_project_details(project_id); - - if project_details.status == ProjectStatus::EvaluationRound { - let evaluation_end = project_details.phase_transition_points.evaluation.end().unwrap(); - let auction_start = evaluation_end.saturating_add(2u32.into()); - let blocks_to_start = auction_start.saturating_sub(self.current_block()); - self.advance_time(blocks_to_start).unwrap(); - }; - - assert_eq!(self.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); - - self.execute(|| crate::Pallet::::do_auction_opening(caller, project_id).unwrap()); - - assert_eq!(self.get_project_details(project_id).status, ProjectStatus::AuctionOpening); - - Ok(()) - } - - pub fn create_auctioning_project( - &mut self, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - ) -> ProjectId { - let project_id = self.create_evaluating_project(project_metadata, issuer.clone()); - - let evaluators = evaluations.accounts(); - let prev_supply = self.get_plmc_total_supply(); - let prev_plmc_balances = self.get_free_plmc_balances_for(evaluators.clone()); - - let plmc_eval_deposits: Vec> = Self::calculate_evaluation_plmc_spent(evaluations.clone()); - let plmc_existential_deposits: Vec> = evaluators.existential_deposits(); - - let expected_remaining_plmc: Vec> = Self::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - self.mint_plmc_to(plmc_eval_deposits.clone()); - self.mint_plmc_to(plmc_existential_deposits.clone()); - - self.evaluate_for_users(project_id, evaluations).unwrap(); - - let expected_evaluator_balances = - Self::sum_balance_mappings(vec![plmc_eval_deposits.clone(), plmc_existential_deposits.clone()]); - - let expected_total_supply = prev_supply + expected_evaluator_balances; - - self.evaluation_assertions(project_id, expected_remaining_plmc, plmc_eval_deposits, expected_total_supply); - - self.start_auction(project_id, issuer).unwrap(); - project_id - } - - pub fn bid_for_users(&mut self, project_id: ProjectId, bids: Vec>) -> DispatchResultWithPostInfo { - for bid in bids { - self.execute(|| { - let did = generate_did_from_account(bid.bidder.clone()); - crate::Pallet::::do_bid( - &bid.bidder, - project_id, - bid.amount, - bid.multiplier, - bid.asset, - did, - InvestorType::Institutional, - ) - })?; - } - Ok(().into()) - } - - pub fn start_community_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { - let opening_end = self - .get_project_details(project_id) - .phase_transition_points - .auction_opening - .end() - .expect("Auction Opening end point should exist"); - - self.execute(|| frame_system::Pallet::::set_block_number(opening_end)); - // run on_initialize - self.advance_time(1u32.into()).unwrap(); - - let closing_end = self - .get_project_details(project_id) - .phase_transition_points - .auction_closing - .end() - .expect("closing end point should exist"); - - self.execute(|| frame_system::Pallet::::set_block_number(closing_end)); - // run on_initialize - self.advance_time(1u32.into()).unwrap(); - - ensure!( - self.get_project_details(project_id).status == ProjectStatus::CommunityRound, - DispatchError::from("Auction failed") - ); - - Ok(()) - } - - pub fn create_community_contributing_project( - &mut self, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - ) -> ProjectId { - if bids.is_empty() { - panic!("Cannot start community funding without bids") - } - - let project_id = self.create_auctioning_project(project_metadata.clone(), issuer, evaluations.clone()); - let bidders = bids.accounts(); - let asset_id = bids[0].asset.to_assethub_id(); - let prev_plmc_balances = self.get_free_plmc_balances_for(bidders.clone()); - let prev_funding_asset_balances = self.get_free_foreign_asset_balances_for(asset_id, bidders.clone()); - let plmc_evaluation_deposits: Vec> = Self::calculate_evaluation_plmc_spent(evaluations); - let plmc_bid_deposits: Vec> = - Self::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &bids, - project_metadata.clone(), - None, - ); - let participation_usable_evaluation_deposits = plmc_evaluation_deposits - .into_iter() - .map(|mut x| { - x.plmc_amount = x.plmc_amount.saturating_sub(::EvaluatorSlash::get() * x.plmc_amount); - x - }) - .collect::>>(); - let necessary_plmc_mint = Self::generic_map_operation( - vec![plmc_bid_deposits.clone(), participation_usable_evaluation_deposits], - MergeOperation::Subtract, - ); - let total_plmc_participation_locked = plmc_bid_deposits; - let plmc_existential_deposits: Vec> = bidders.existential_deposits(); - let funding_asset_deposits = Self::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &bids, - project_metadata.clone(), - None, - ); - - let bidder_balances = - Self::sum_balance_mappings(vec![necessary_plmc_mint.clone(), plmc_existential_deposits.clone()]); - - let expected_free_plmc_balances = Self::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - let prev_supply = self.get_plmc_total_supply(); - let post_supply = prev_supply + bidder_balances; - - self.mint_plmc_to(necessary_plmc_mint.clone()); - self.mint_plmc_to(plmc_existential_deposits.clone()); - self.mint_foreign_asset_to(funding_asset_deposits.clone()); - - self.bid_for_users(project_id, bids.clone()).unwrap(); - - self.do_reserved_plmc_assertions( - total_plmc_participation_locked.merge_accounts(MergeOperation::Add), - HoldReason::Participation(project_id).into(), - ); - self.do_bid_transferred_foreign_asset_assertions( - funding_asset_deposits.merge_accounts(MergeOperation::Add), - project_id, - ); - self.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); - self.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); - assert_eq!(self.get_plmc_total_supply(), post_supply); - - self.start_community_funding(project_id).unwrap(); - - project_id - } - - pub fn contribute_for_users( - &mut self, - project_id: ProjectId, - contributions: Vec>, - ) -> DispatchResultWithPostInfo { - match self.get_project_details(project_id).status { - ProjectStatus::CommunityRound => - for cont in contributions { - let did = generate_did_from_account(cont.contributor.clone()); - let investor_type = InvestorType::Retail; - self.execute(|| { - crate::Pallet::::do_community_contribute( - &cont.contributor, - project_id, - cont.amount, - cont.multiplier, - cont.asset, - did, - investor_type, - ) - })?; - }, - ProjectStatus::RemainderRound => - for cont in contributions { - let did = generate_did_from_account(cont.contributor.clone()); - let investor_type = InvestorType::Professional; - self.execute(|| { - crate::Pallet::::do_remaining_contribute( - &cont.contributor, - project_id, - cont.amount, - cont.multiplier, - cont.asset, - did, - investor_type, - ) - })?; - }, - _ => panic!("Project should be in Community or Remainder status"), - } - - Ok(().into()) - } - - pub fn start_remainder_or_end_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { - let details = self.get_project_details(project_id); - assert_eq!(details.status, ProjectStatus::CommunityRound); - let remaining_tokens = details.remaining_contribution_tokens; - let update_type = - if remaining_tokens > Zero::zero() { UpdateType::RemainderFundingStart } else { UpdateType::FundingEnd }; - if let Some(transition_block) = self.get_update_block(project_id, &update_type) { - self.execute(|| frame_system::Pallet::::set_block_number(transition_block - One::one())); - self.advance_time(1u32.into()).unwrap(); - match self.get_project_details(project_id).status { - ProjectStatus::RemainderRound | ProjectStatus::FundingSuccessful => Ok(()), - _ => panic!("Bad state"), - } - } else { - panic!("Bad state") - } - } - - pub fn finish_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { - if let Some(update_block) = self.get_update_block(project_id, &UpdateType::RemainderFundingStart) { - self.execute(|| frame_system::Pallet::::set_block_number(update_block - One::one())); - self.advance_time(1u32.into()).unwrap(); - } - let update_block = - self.get_update_block(project_id, &UpdateType::FundingEnd).expect("Funding end block should exist"); - self.execute(|| frame_system::Pallet::::set_block_number(update_block - One::one())); - self.advance_time(1u32.into()).unwrap(); - let project_details = self.get_project_details(project_id); - assert!( - matches!( - project_details.status, - ProjectStatus::FundingSuccessful | - ProjectStatus::FundingFailed | - ProjectStatus::AwaitingProjectDecision - ), - "Project should be in Finished status" - ); - Ok(()) - } - - pub fn settle_project(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { - let details = self.get_project_details(project_id); - self.execute(|| match details.status { - ProjectStatus::FundingSuccessful => Self::settle_successful_project(project_id), - ProjectStatus::FundingFailed => Self::settle_failed_project(project_id), - _ => panic!("Project should be in FundingSuccessful or FundingFailed status"), - }) - } - - fn settle_successful_project(project_id: ProjectId) -> Result<(), DispatchError> { - Evaluations::::iter_prefix((project_id,)) - .try_for_each(|(_, evaluation)| Pallet::::do_settle_successful_evaluation(evaluation, project_id))?; - - Bids::::iter_prefix((project_id,)) - .try_for_each(|(_, bid)| Pallet::::do_settle_successful_bid(bid, project_id))?; - - Contributions::::iter_prefix((project_id,)) - .try_for_each(|(_, contribution)| Pallet::::do_settle_successful_contribution(contribution, project_id)) - } - - fn settle_failed_project(project_id: ProjectId) -> Result<(), DispatchError> { - Evaluations::::iter_prefix((project_id,)) - .try_for_each(|(_, evaluation)| Pallet::::do_settle_failed_evaluation(evaluation, project_id))?; - - Bids::::iter_prefix((project_id,)) - .try_for_each(|(_, bid)| Pallet::::do_settle_failed_bid(bid, project_id))?; - - Contributions::::iter_prefix((project_id,)) - .try_for_each(|(_, contribution)| Pallet::::do_settle_failed_contribution(contribution, project_id))?; - - Ok(()) - } - - pub fn get_evaluations(&mut self, project_id: ProjectId) -> Vec> { - self.execute(|| Evaluations::::iter_prefix_values((project_id,)).collect()) - } - - pub fn get_bids(&mut self, project_id: ProjectId) -> Vec> { - self.execute(|| Bids::::iter_prefix_values((project_id,)).collect()) - } - - pub fn get_contributions(&mut self, project_id: ProjectId) -> Vec> { - self.execute(|| Contributions::::iter_prefix_values((project_id,)).collect()) - } - - // Used to check if all evaluations are settled correctly. We cannot check amount of - // contributions minted for the user, as they could have received more tokens from other participations. - pub fn assert_evaluations_migrations_created( - &mut self, - project_id: ProjectId, - evaluations: Vec>, - percentage: u64, - ) { - let details = self.get_project_details(project_id); - assert!(matches!(details.status, ProjectStatus::FundingSuccessful | ProjectStatus::FundingFailed)); - - self.execute(|| { - for evaluation in evaluations { - let reward_info = - ProjectsDetails::::get(project_id).unwrap().evaluation_round_info.evaluators_outcome; - let account = evaluation.evaluator.clone(); - assert_eq!(Evaluations::::iter_prefix_values((&project_id, &account)).count(), 0); - - let (amount, should_exist) = match percentage { - 0..=75 => { - assert!(matches!(reward_info, EvaluatorsOutcome::Slashed)); - (0u64.into(), false) - }, - 76..=89 => { - assert!(matches!(reward_info, EvaluatorsOutcome::Unchanged)); - (0u64.into(), false) - }, - 90..=100 => { - let reward = match reward_info { - EvaluatorsOutcome::Rewarded(info) => - Pallet::::calculate_evaluator_reward(&evaluation, &info), - _ => panic!("Evaluators should be rewarded"), - }; - (reward, true) - }, - _ => panic!("Percentage should be between 0 and 100"), - }; - Self::assert_migration( - project_id, - account, - amount, - evaluation.id, - ParticipationType::Evaluation, - should_exist, - ); - } - }); - } - - // Testing if a list of bids are settled correctly. - pub fn assert_bids_migrations_created( - &mut self, - project_id: ProjectId, - bids: Vec>, - is_successful: bool, - ) { - self.execute(|| { - for bid in bids { - let account = bid.bidder.clone(); - assert_eq!(Bids::::iter_prefix_values((&project_id, &account)).count(), 0); - let amount: BalanceOf = if is_successful { bid.final_ct_amount } else { 0u64.into() }; - Self::assert_migration(project_id, account, amount, bid.id, ParticipationType::Bid, is_successful); - } - }); - } - - // Testing if a list of contributions are settled correctly. - pub fn assert_contributions_migrations_created( - &mut self, - project_id: ProjectId, - contributions: Vec>, - is_successful: bool, - ) { - self.execute(|| { - for contribution in contributions { - let account = contribution.contributor.clone(); - assert_eq!(Bids::::iter_prefix_values((&project_id, &account)).count(), 0); - let amount: BalanceOf = if is_successful { contribution.ct_amount } else { 0u64.into() }; - Self::assert_migration( - project_id, - account, - amount, - contribution.id, - ParticipationType::Contribution, - is_successful, - ); - } - }); - } - - fn assert_migration( - project_id: ProjectId, - account: AccountIdOf, - amount: BalanceOf, - id: u32, - participation_type: ParticipationType, - should_exist: bool, - ) { - let correct = match (should_exist, UserMigrations::::get(project_id, account.clone())) { - // User has migrations, so we need to check if any matches our criteria - (_, Some((_, migrations))) => { - let maybe_migration = migrations.into_iter().find(|migration| { - let user = T::AccountId32Conversion::convert(account.clone()); - matches!(migration.origin, MigrationOrigin { user: m_user, id: m_id, participation_type: m_participation_type } if m_user == user && m_id == id && m_participation_type == participation_type) - }); - match maybe_migration { - // Migration exists so we check if the amount is correct and if it should exist - Some(migration) => migration.info.contribution_token_amount == amount.into() && should_exist, - // Migration doesn't exist so we check if it should not exist - None => !should_exist, - } - }, - // User does not have any migrations, so the migration should not exist - (false, None) => true, - (true, None) => false, - }; - assert!(correct); - } - - pub fn create_remainder_contributing_project( - &mut self, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - contributions: Vec>, - ) -> ProjectId { - let project_id = self.create_community_contributing_project( - project_metadata.clone(), - issuer, - evaluations.clone(), - bids.clone(), - ); - - if contributions.is_empty() { - self.start_remainder_or_end_funding(project_id).unwrap(); - return project_id; - } - - let ct_price = self.get_project_details(project_id).weighted_average_price.unwrap(); - - let contributors = contributions.accounts(); - - let asset_id = contributions[0].asset.to_assethub_id(); - - let prev_plmc_balances = self.get_free_plmc_balances_for(contributors.clone()); - let prev_funding_asset_balances = self.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); - - let plmc_evaluation_deposits = Self::calculate_evaluation_plmc_spent(evaluations.clone()); - let plmc_bid_deposits = Self::calculate_auction_plmc_spent_post_wap(&bids, project_metadata.clone(), ct_price); - let plmc_contribution_deposits = Self::calculate_contributed_plmc_spent(contributions.clone(), ct_price); - - let reducible_evaluator_balances = Self::slash_evaluator_balances(plmc_evaluation_deposits.clone()); - let necessary_plmc_mint = Self::generic_map_operation( - vec![plmc_contribution_deposits.clone(), reducible_evaluator_balances], - MergeOperation::Subtract, - ); - let total_plmc_participation_locked = - Self::generic_map_operation(vec![plmc_bid_deposits, plmc_contribution_deposits], MergeOperation::Add); - let plmc_existential_deposits = contributors.existential_deposits(); - - let funding_asset_deposits = Self::calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); - let contributor_balances = - Self::sum_balance_mappings(vec![necessary_plmc_mint.clone(), plmc_existential_deposits.clone()]); - - let expected_free_plmc_balances = Self::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - let prev_supply = self.get_plmc_total_supply(); - let post_supply = prev_supply + contributor_balances; - - self.mint_plmc_to(necessary_plmc_mint.clone()); - self.mint_plmc_to(plmc_existential_deposits.clone()); - self.mint_foreign_asset_to(funding_asset_deposits.clone()); - - self.contribute_for_users(project_id, contributions).expect("Contributing should work"); - - self.do_reserved_plmc_assertions( - total_plmc_participation_locked.merge_accounts(MergeOperation::Add), - HoldReason::Participation(project_id).into(), - ); - - self.do_contribution_transferred_foreign_asset_assertions(funding_asset_deposits, project_id); - - self.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); - self.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); - assert_eq!(self.get_plmc_total_supply(), post_supply); - - self.start_remainder_or_end_funding(project_id).unwrap(); - - project_id - } - - pub fn create_finished_project( - &mut self, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - community_contributions: Vec>, - remainder_contributions: Vec>, - ) -> ProjectId { - let project_id = self.create_remainder_contributing_project( - project_metadata.clone(), - issuer, - evaluations.clone(), - bids.clone(), - community_contributions.clone(), - ); - - match self.get_project_details(project_id).status { - ProjectStatus::FundingSuccessful => return project_id, - ProjectStatus::RemainderRound if remainder_contributions.is_empty() => { - self.finish_funding(project_id).unwrap(); - return project_id; - }, - _ => {}, - }; - - let ct_price = self.get_project_details(project_id).weighted_average_price.unwrap(); - let contributors = remainder_contributions.accounts(); - let asset_id = remainder_contributions[0].asset.to_assethub_id(); - let prev_plmc_balances = self.get_free_plmc_balances_for(contributors.clone()); - let prev_funding_asset_balances = self.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); - - let plmc_evaluation_deposits = Self::calculate_evaluation_plmc_spent(evaluations); - let plmc_bid_deposits = Self::calculate_auction_plmc_spent_post_wap(&bids, project_metadata.clone(), ct_price); - let plmc_community_contribution_deposits = - Self::calculate_contributed_plmc_spent(community_contributions.clone(), ct_price); - let plmc_remainder_contribution_deposits = - Self::calculate_contributed_plmc_spent(remainder_contributions.clone(), ct_price); - - let necessary_plmc_mint = Self::generic_map_operation( - vec![plmc_remainder_contribution_deposits.clone(), plmc_evaluation_deposits], - MergeOperation::Subtract, - ); - let total_plmc_participation_locked = Self::generic_map_operation( - vec![plmc_bid_deposits, plmc_community_contribution_deposits, plmc_remainder_contribution_deposits], - MergeOperation::Add, - ); - let plmc_existential_deposits = contributors.existential_deposits(); - let funding_asset_deposits = - Self::calculate_contributed_funding_asset_spent(remainder_contributions.clone(), ct_price); - - let contributor_balances = - Self::sum_balance_mappings(vec![necessary_plmc_mint.clone(), plmc_existential_deposits.clone()]); - - let expected_free_plmc_balances = Self::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - let prev_supply = self.get_plmc_total_supply(); - let post_supply = prev_supply + contributor_balances; - - self.mint_plmc_to(necessary_plmc_mint.clone()); - self.mint_plmc_to(plmc_existential_deposits.clone()); - self.mint_foreign_asset_to(funding_asset_deposits.clone()); - - self.contribute_for_users(project_id, remainder_contributions.clone()) - .expect("Remainder Contributing should work"); - - self.do_reserved_plmc_assertions( - total_plmc_participation_locked.merge_accounts(MergeOperation::Add), - HoldReason::Participation(project_id).into(), - ); - self.do_contribution_transferred_foreign_asset_assertions( - funding_asset_deposits.merge_accounts(MergeOperation::Add), - project_id, - ); - self.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); - self.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); - assert_eq!(self.get_plmc_total_supply(), post_supply); - - self.finish_funding(project_id).unwrap(); - - if self.get_project_details(project_id).status == ProjectStatus::FundingSuccessful { - // Check that remaining CTs are updated - let project_details = self.get_project_details(project_id); - // if our bids were creating an oversubscription, then just take the total allocation size - let auction_bought_tokens = bids - .iter() - .map(|bid| bid.amount) - .fold(Zero::zero(), |acc, item| item + acc) - .min(project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size); - let community_bought_tokens = - community_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); - let remainder_bought_tokens = - remainder_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); - - assert_eq!( - project_details.remaining_contribution_tokens, - project_metadata.total_allocation_size - - auction_bought_tokens - - community_bought_tokens - - remainder_bought_tokens, - "Remaining CTs are incorrect" - ); - } - - project_id - } - - pub fn create_project_at( - &mut self, - status: ProjectStatus, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - community_contributions: Vec>, - remainder_contributions: Vec>, - ) -> ProjectId { - match status { - ProjectStatus::FundingSuccessful => self.create_finished_project( - project_metadata, - issuer, - evaluations, - bids, - community_contributions, - remainder_contributions, - ), - ProjectStatus::RemainderRound => self.create_remainder_contributing_project( - project_metadata, - issuer, - evaluations, - bids, - community_contributions, - ), - ProjectStatus::CommunityRound => - self.create_community_contributing_project(project_metadata, issuer, evaluations, bids), - ProjectStatus::AuctionOpening => self.create_auctioning_project(project_metadata, issuer, evaluations), - ProjectStatus::EvaluationRound => self.create_evaluating_project(project_metadata, issuer), - ProjectStatus::Application => self.create_new_project(project_metadata, issuer), - _ => panic!("unsupported project creation in that status"), - } - } -} - -#[cfg(feature = "std")] -pub mod async_features { - use super::*; - use assert_matches2::assert_matches; - use futures::FutureExt; - use std::{ - collections::HashMap, - sync::{ - atomic::{AtomicBool, AtomicU32, Ordering}, - Arc, - }, - time::Duration, - }; - use tokio::{ - sync::{Mutex, Notify}, - time::sleep, - }; - - pub struct BlockOrchestrator { - pub current_block: Arc, - // used for resuming execution of a project that is waiting for a certain block to be reached - pub awaiting_projects: Mutex, Vec>>>, - pub should_continue: Arc, - pub instantiator_phantom: PhantomData<(T, AllPalletsWithoutSystem, RuntimeEvent)>, - } - pub async fn block_controller< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - block_orchestrator: Arc>, - instantiator: Arc>>, - ) { - loop { - if !block_orchestrator.continue_running() { - break; - } - - let maybe_target_reached = block_orchestrator.advance_to_next_target(instantiator.clone()).await; - - if let Some(target_reached) = maybe_target_reached { - block_orchestrator.execute_callbacks(target_reached).await; - } - // leaves some time for the projects to submit their targets to the orchestrator - sleep(Duration::from_millis(100)).await; - } - } - - impl< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - > Default for BlockOrchestrator - { - fn default() -> Self { - Self::new() - } - } - - impl< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - > BlockOrchestrator - { - pub fn new() -> Self { - BlockOrchestrator:: { - current_block: Arc::new(AtomicU32::new(0)), - awaiting_projects: Mutex::new(HashMap::new()), - should_continue: Arc::new(AtomicBool::new(true)), - instantiator_phantom: PhantomData, - } - } - - pub async fn add_awaiting_project(&self, block_number: BlockNumberFor, notify: Arc) { - let mut awaiting_projects = self.awaiting_projects.lock().await; - awaiting_projects.entry(block_number).or_default().push(notify); - drop(awaiting_projects); - } - - pub async fn advance_to_next_target( - &self, - instantiator: Arc>>, - ) -> Option> { - let mut inst = instantiator.lock().await; - let now: u32 = - inst.current_block().try_into().unwrap_or_else(|_| panic!("Block number should fit into u32")); - self.current_block.store(now, Ordering::SeqCst); - - let awaiting_projects = self.awaiting_projects.lock().await; - - if let Some(&next_block) = awaiting_projects.keys().min() { - drop(awaiting_projects); - - while self.get_current_block() < next_block { - inst.advance_time(One::one()).unwrap(); - let current_block: u32 = self - .get_current_block() - .try_into() - .unwrap_or_else(|_| panic!("Block number should fit into u32")); - self.current_block.store(current_block + 1u32, Ordering::SeqCst); - } - Some(next_block) - } else { - None - } - } - - pub async fn execute_callbacks(&self, block_number: BlockNumberFor) { - let mut awaiting_projects = self.awaiting_projects.lock().await; - if let Some(notifies) = awaiting_projects.remove(&block_number) { - for notify in notifies { - notify.notify_one(); - } - } - } - - pub async fn is_empty(&self) -> bool { - self.awaiting_projects.lock().await.is_empty() - } - - // Method to check if the loop should continue - pub fn continue_running(&self) -> bool { - self.should_continue.load(Ordering::SeqCst) - } - - // Method to get the current block number - pub fn get_current_block(&self) -> BlockNumberFor { - self.current_block.load(Ordering::SeqCst).into() - } - } - - // async instantiations for parallel testing - pub async fn async_create_new_project< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - ) -> ProjectId { - let mut inst = instantiator.lock().await; - - let now = inst.current_block(); - // One ED for the issuer, one for the escrow account - inst.mint_plmc_to(vec![UserToPLMCBalance::new( - issuer.clone(), - Instantiator::::get_ed() * 2u64.into(), - )]); - inst.execute(|| { - crate::Pallet::::do_create_project( - &issuer.clone(), - project_metadata.clone(), - generate_did_from_account(issuer.clone()), - ) - .unwrap(); - let last_project_metadata = ProjectsMetadata::::iter().last().unwrap(); - log::trace!("Last project metadata: {:?}", last_project_metadata); - }); - - let created_project_id = inst.execute(|| NextProjectId::::get().saturating_sub(One::one())); - inst.creation_assertions(created_project_id, project_metadata, now); - created_project_id - } - - pub async fn async_create_evaluating_project< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - ) -> ProjectId { - let project_id = async_create_new_project(instantiator.clone(), project_metadata, issuer.clone()).await; - - let mut inst = instantiator.lock().await; - - inst.start_evaluation(project_id, issuer).unwrap(); - project_id - } - - pub async fn async_start_auction< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_id: ProjectId, - caller: AccountIdOf, - ) -> Result<(), DispatchError> { - let mut inst = instantiator.lock().await; - - let project_details = inst.get_project_details(project_id); - - if project_details.status == ProjectStatus::EvaluationRound { - let update_block = inst.get_update_block(project_id, &UpdateType::EvaluationEnd).unwrap(); - let notify = Arc::new(Notify::new()); - block_orchestrator.add_awaiting_project(update_block + 1u32.into(), notify.clone()).await; - - // Wait for the notification that our desired block was reached to continue - drop(inst); - - notify.notified().await; - - inst = instantiator.lock().await; - }; - - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); - - inst.execute(|| crate::Pallet::::do_auction_opening(caller.clone(), project_id).unwrap()); - - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionOpening); - - Ok(()) - } - - pub async fn async_create_auctioning_project< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - ) -> ProjectId { - let project_id = - async_create_evaluating_project(instantiator.clone(), project_metadata.clone(), issuer.clone()).await; - - let mut inst = instantiator.lock().await; - - let evaluators = evaluations.accounts(); - let prev_supply = inst.get_plmc_total_supply(); - let prev_plmc_balances = inst.get_free_plmc_balances_for(evaluators.clone()); - - let plmc_eval_deposits: Vec> = - Instantiator::::calculate_evaluation_plmc_spent( - evaluations.clone(), - ); - let plmc_existential_deposits: Vec> = evaluators.existential_deposits(); - - let expected_remaining_plmc: Vec> = - Instantiator::::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - inst.mint_plmc_to(plmc_eval_deposits.clone()); - inst.mint_plmc_to(plmc_existential_deposits.clone()); - - inst.evaluate_for_users(project_id, evaluations).unwrap(); - - let expected_evaluator_balances = - Instantiator::::sum_balance_mappings(vec![ - plmc_eval_deposits.clone(), - plmc_existential_deposits.clone(), - ]); - - let expected_total_supply = prev_supply + expected_evaluator_balances; - - inst.evaluation_assertions(project_id, expected_remaining_plmc, plmc_eval_deposits, expected_total_supply); - - drop(inst); - - async_start_auction(instantiator.clone(), block_orchestrator, project_id, issuer).await.unwrap(); - - inst = instantiator.lock().await; - let plmc_for_bids = - Instantiator::::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &bids, - project_metadata.clone(), - None - ); - let plmc_existential_deposits: Vec> = bids.accounts().existential_deposits(); - let usdt_for_bids = - Instantiator::::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &bids, - project_metadata, - None - ); - - inst.mint_plmc_to(plmc_for_bids.clone()); - inst.mint_plmc_to(plmc_existential_deposits.clone()); - inst.mint_foreign_asset_to(usdt_for_bids.clone()); - - inst.bid_for_users(project_id, bids).unwrap(); - drop(inst); - - project_id - } - - pub async fn async_start_community_funding< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_id: ProjectId, - ) -> Result<(), DispatchError> { - let mut inst = instantiator.lock().await; - - let update_block = inst.get_update_block(project_id, &UpdateType::AuctionClosingStart).unwrap(); - let closing_start = update_block + 1u32.into(); - - let notify = Arc::new(Notify::new()); - - block_orchestrator.add_awaiting_project(closing_start, notify.clone()).await; - - // Wait for the notification that our desired block was reached to continue - - drop(inst); - - notify.notified().await; - - inst = instantiator.lock().await; - let update_block = inst.get_update_block(project_id, &UpdateType::CommunityFundingStart).unwrap(); - let community_start = update_block + 1u32.into(); - - let notify = Arc::new(Notify::new()); - - block_orchestrator.add_awaiting_project(community_start, notify.clone()).await; - - drop(inst); - - notify.notified().await; - - inst = instantiator.lock().await; - - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::CommunityRound); - - Ok(()) - } - - pub async fn async_create_community_contributing_project< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - ) -> (ProjectId, Vec>) { - if bids.is_empty() { - panic!("Cannot start community funding without bids") - } - - let project_id = async_create_auctioning_project( - instantiator.clone(), - block_orchestrator.clone(), - project_metadata.clone(), - issuer, - evaluations.clone(), - vec![], - ) - .await; - - let mut inst = instantiator.lock().await; - - let bidders = bids.accounts(); - let asset_id = bids[0].asset.to_assethub_id(); - let prev_plmc_balances = inst.get_free_plmc_balances_for(bidders.clone()); - let prev_funding_asset_balances = inst.get_free_foreign_asset_balances_for(asset_id, bidders.clone()); - let plmc_evaluation_deposits: Vec> = - Instantiator::::calculate_evaluation_plmc_spent(evaluations); - let plmc_bid_deposits: Vec> = - Instantiator::::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &bids, - project_metadata.clone(), - None - ); - let participation_usable_evaluation_deposits = plmc_evaluation_deposits - .into_iter() - .map(|mut x| { - x.plmc_amount = x.plmc_amount.saturating_sub(::EvaluatorSlash::get() * x.plmc_amount); - x - }) - .collect::>>(); - let necessary_plmc_mint = Instantiator::::generic_map_operation( - vec![plmc_bid_deposits.clone(), participation_usable_evaluation_deposits], - MergeOperation::Subtract, - ); - let total_plmc_participation_locked = plmc_bid_deposits; - let plmc_existential_deposits: Vec> = bidders.existential_deposits(); - let funding_asset_deposits = - Instantiator::::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &bids, - project_metadata.clone(), - None - ); - - let bidder_balances = Instantiator::::sum_balance_mappings(vec![ - necessary_plmc_mint.clone(), - plmc_existential_deposits.clone(), - ]); - - let expected_free_plmc_balances = - Instantiator::::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - let prev_supply = inst.get_plmc_total_supply(); - let post_supply = prev_supply + bidder_balances; - - inst.mint_plmc_to(necessary_plmc_mint.clone()); - inst.mint_plmc_to(plmc_existential_deposits.clone()); - inst.mint_foreign_asset_to(funding_asset_deposits.clone()); - - inst.bid_for_users(project_id, bids.clone()).unwrap(); - - inst.do_reserved_plmc_assertions( - total_plmc_participation_locked.merge_accounts(MergeOperation::Add), - HoldReason::Participation(project_id).into(), - ); - inst.do_bid_transferred_foreign_asset_assertions( - funding_asset_deposits.merge_accounts(MergeOperation::Add), - project_id, - ); - inst.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); - inst.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); - assert_eq!(inst.get_plmc_total_supply(), post_supply); - - drop(inst); - async_start_community_funding(instantiator.clone(), block_orchestrator, project_id).await.unwrap(); - let mut inst = instantiator.lock().await; - - let _weighted_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); - let accepted_bids = Instantiator::::filter_bids_after_auction( - bids, - project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size, - ); - let bid_expectations = accepted_bids - .iter() - .map(|bid| BidInfoFilter:: { - bidder: Some(bid.bidder.clone()), - final_ct_amount: Some(bid.amount), - ..Default::default() - }) - .collect_vec(); - - let total_ct_sold = accepted_bids.iter().map(|bid| bid.amount).fold(Zero::zero(), |acc, item| item + acc); - - inst.finalized_bids_assertions(project_id, bid_expectations, total_ct_sold); - - (project_id, accepted_bids) - } - - pub async fn async_start_remainder_or_end_funding< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_id: ProjectId, - ) -> Result<(), DispatchError> { - let mut inst = instantiator.lock().await; - - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::CommunityRound); - - let update_block = inst.get_update_block(project_id, &UpdateType::RemainderFundingStart).unwrap(); - let remainder_start = update_block + 1u32.into(); - - let notify = Arc::new(Notify::new()); - - block_orchestrator.add_awaiting_project(remainder_start, notify.clone()).await; - - // Wait for the notification that our desired block was reached to continue - - drop(inst); - - notify.notified().await; - - let mut inst = instantiator.lock().await; - - assert_matches!( - inst.get_project_details(project_id).status, - (ProjectStatus::RemainderRound | ProjectStatus::FundingSuccessful) - ); - Ok(()) - } - - pub async fn async_create_remainder_contributing_project< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - contributions: Vec>, - ) -> (ProjectId, Vec>) { - let (project_id, accepted_bids) = async_create_community_contributing_project( - instantiator.clone(), - block_orchestrator.clone(), - project_metadata, - issuer, - evaluations.clone(), - bids, - ) - .await; - - if contributions.is_empty() { - async_start_remainder_or_end_funding(instantiator.clone(), block_orchestrator.clone(), project_id) - .await - .unwrap(); - return (project_id, accepted_bids); - } - - let mut inst = instantiator.lock().await; - - let ct_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); - let contributors = contributions.accounts(); - let asset_id = contributions[0].asset.to_assethub_id(); - let prev_plmc_balances = inst.get_free_plmc_balances_for(contributors.clone()); - let prev_funding_asset_balances = inst.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); - - let plmc_evaluation_deposits = - Instantiator::::calculate_evaluation_plmc_spent(evaluations); - let plmc_bid_deposits = - Instantiator::::calculate_auction_plmc_charged_with_given_price( - &accepted_bids, - ct_price, - ); - - let plmc_contribution_deposits = - Instantiator::::calculate_contributed_plmc_spent( - contributions.clone(), - ct_price, - ); - - let necessary_plmc_mint = Instantiator::::generic_map_operation( - vec![plmc_contribution_deposits.clone(), plmc_evaluation_deposits], - MergeOperation::Subtract, - ); - let total_plmc_participation_locked = - Instantiator::::generic_map_operation( - vec![plmc_bid_deposits, plmc_contribution_deposits], - MergeOperation::Add, - ); - let plmc_existential_deposits = contributors.existential_deposits(); - - let funding_asset_deposits = - Instantiator::::calculate_contributed_funding_asset_spent( - contributions.clone(), - ct_price, - ); - let contributor_balances = - Instantiator::::sum_balance_mappings(vec![ - necessary_plmc_mint.clone(), - plmc_existential_deposits.clone(), - ]); - - let expected_free_plmc_balances = - Instantiator::::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - let prev_supply = inst.get_plmc_total_supply(); - let post_supply = prev_supply + contributor_balances; - - inst.mint_plmc_to(necessary_plmc_mint.clone()); - inst.mint_plmc_to(plmc_existential_deposits.clone()); - inst.mint_foreign_asset_to(funding_asset_deposits.clone()); - - inst.contribute_for_users(project_id, contributions).expect("Contributing should work"); - - inst.do_reserved_plmc_assertions( - total_plmc_participation_locked.merge_accounts(MergeOperation::Add), - HoldReason::Participation(project_id).into(), - ); - inst.do_contribution_transferred_foreign_asset_assertions( - funding_asset_deposits.merge_accounts(MergeOperation::Add), - project_id, - ); - inst.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); - inst.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); - assert_eq!(inst.get_plmc_total_supply(), post_supply); - drop(inst); - async_start_remainder_or_end_funding(instantiator.clone(), block_orchestrator.clone(), project_id) - .await - .unwrap(); - (project_id, accepted_bids) - } - - pub async fn async_finish_funding< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_id: ProjectId, - ) -> Result<(), DispatchError> { - let mut inst = instantiator.lock().await; - let update_block = inst.get_update_block(project_id, &UpdateType::FundingEnd).unwrap(); - - let notify = Arc::new(Notify::new()); - block_orchestrator.add_awaiting_project(update_block + 1u32.into(), notify.clone()).await; - Ok(()) - } - - pub async fn async_create_finished_project< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - project_metadata: ProjectMetadataOf, - issuer: AccountIdOf, - evaluations: Vec>, - bids: Vec>, - community_contributions: Vec>, - remainder_contributions: Vec>, - ) -> ProjectId { - let (project_id, accepted_bids) = async_create_remainder_contributing_project( - instantiator.clone(), - block_orchestrator.clone(), - project_metadata.clone(), - issuer, - evaluations.clone(), - bids.clone(), - community_contributions.clone(), - ) - .await; - - let mut inst = instantiator.lock().await; - - let total_ct_sold_in_bids = bids.iter().map(|bid| bid.amount).fold(Zero::zero(), |acc, item| item + acc); - let total_ct_sold_in_community_contributions = - community_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); - let total_ct_sold_in_remainder_contributions = - remainder_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); - - let total_ct_sold = - total_ct_sold_in_bids + total_ct_sold_in_community_contributions + total_ct_sold_in_remainder_contributions; - let total_ct_available = project_metadata.total_allocation_size; - assert!( - total_ct_sold <= total_ct_available, - "Some CT buys are getting less than expected due to running out of CTs. This is ok in the runtime, but likely unexpected from the parameters of this instantiation" - ); - - match inst.get_project_details(project_id).status { - ProjectStatus::FundingSuccessful => return project_id, - ProjectStatus::RemainderRound if remainder_contributions.is_empty() => { - drop(inst); - async_finish_funding(instantiator.clone(), block_orchestrator.clone(), project_id).await.unwrap(); - return project_id; - }, - _ => {}, - }; - - let ct_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); - let contributors = remainder_contributions.accounts(); - let asset_id = remainder_contributions[0].asset.to_assethub_id(); - let prev_plmc_balances = inst.get_free_plmc_balances_for(contributors.clone()); - let prev_funding_asset_balances = inst.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); - - let plmc_evaluation_deposits = - Instantiator::::calculate_evaluation_plmc_spent(evaluations); - let plmc_bid_deposits = - Instantiator::::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &accepted_bids, - project_metadata.clone(), - None - ); - let plmc_community_contribution_deposits = - Instantiator::::calculate_contributed_plmc_spent( - community_contributions.clone(), - ct_price, - ); - let plmc_remainder_contribution_deposits = - Instantiator::::calculate_contributed_plmc_spent( - remainder_contributions.clone(), - ct_price, - ); - - let necessary_plmc_mint = Instantiator::::generic_map_operation( - vec![plmc_remainder_contribution_deposits.clone(), plmc_evaluation_deposits], - MergeOperation::Subtract, - ); - let total_plmc_participation_locked = - Instantiator::::generic_map_operation( - vec![plmc_bid_deposits, plmc_community_contribution_deposits, plmc_remainder_contribution_deposits], - MergeOperation::Add, - ); - let plmc_existential_deposits = contributors.existential_deposits(); - let funding_asset_deposits = - Instantiator::::calculate_contributed_funding_asset_spent( - remainder_contributions.clone(), - ct_price, - ); - - let contributor_balances = - Instantiator::::sum_balance_mappings(vec![ - necessary_plmc_mint.clone(), - plmc_existential_deposits.clone(), - ]); - - let expected_free_plmc_balances = - Instantiator::::generic_map_operation( - vec![prev_plmc_balances, plmc_existential_deposits.clone()], - MergeOperation::Add, - ); - - let prev_supply = inst.get_plmc_total_supply(); - let post_supply = prev_supply + contributor_balances; - - inst.mint_plmc_to(necessary_plmc_mint.clone()); - inst.mint_plmc_to(plmc_existential_deposits.clone()); - inst.mint_foreign_asset_to(funding_asset_deposits.clone()); - - inst.contribute_for_users(project_id, remainder_contributions.clone()) - .expect("Remainder Contributing should work"); - - let merged = total_plmc_participation_locked.merge_accounts(MergeOperation::Add); - - inst.do_reserved_plmc_assertions(merged, HoldReason::Participation(project_id).into()); - - inst.do_contribution_transferred_foreign_asset_assertions( - funding_asset_deposits.merge_accounts(MergeOperation::Add), - project_id, - ); - inst.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); - inst.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); - assert_eq!(inst.get_plmc_total_supply(), post_supply); - - drop(inst); - async_finish_funding(instantiator.clone(), block_orchestrator.clone(), project_id).await.unwrap(); - let mut inst = instantiator.lock().await; - - if inst.get_project_details(project_id).status == ProjectStatus::FundingSuccessful { - // Check that remaining CTs are updated - let project_details = inst.get_project_details(project_id); - let auction_bought_tokens = - accepted_bids.iter().map(|bid| bid.amount).fold(Zero::zero(), |acc, item| item + acc); - let community_bought_tokens = - community_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); - let remainder_bought_tokens = - remainder_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); - - assert_eq!( - project_details.remaining_contribution_tokens, - project_metadata.total_allocation_size - - auction_bought_tokens - - community_bought_tokens - - remainder_bought_tokens, - "Remaining CTs are incorrect" - ); - } - - project_id - } - - pub async fn create_project_at< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Arc>>, - block_orchestrator: Arc>, - test_project_params: TestProjectParams, - ) -> ProjectId { - match test_project_params.expected_state { - ProjectStatus::FundingSuccessful => - async_create_finished_project( - instantiator, - block_orchestrator, - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - test_project_params.community_contributions, - test_project_params.remainder_contributions, - ) - .await, - ProjectStatus::RemainderRound => - async_create_remainder_contributing_project( - instantiator, - block_orchestrator, - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - test_project_params.community_contributions, - ) - .map(|(project_id, _)| project_id) - .await, - ProjectStatus::CommunityRound => - async_create_community_contributing_project( - instantiator, - block_orchestrator, - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - ) - .map(|(project_id, _)| project_id) - .await, - ProjectStatus::AuctionOpening => - async_create_auctioning_project( - instantiator, - block_orchestrator, - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - ) - .await, - ProjectStatus::EvaluationRound => - async_create_evaluating_project(instantiator, test_project_params.metadata, test_project_params.issuer) - .await, - ProjectStatus::Application => - async_create_new_project(instantiator, test_project_params.metadata, test_project_params.issuer).await, - _ => panic!("unsupported project creation in that status"), - } - } - - pub async fn async_create_project_at< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - mutex_inst: Arc>>, - block_orchestrator: Arc>, - test_project_params: TestProjectParams, - ) -> ProjectId { - let time_to_new_project: BlockNumberFor = Zero::zero(); - let time_to_evaluation: BlockNumberFor = time_to_new_project + Zero::zero(); - // we immediately start the auction, so we dont wait for T::AuctionInitializePeriodDuration. - let time_to_auction: BlockNumberFor = time_to_evaluation + ::EvaluationDuration::get(); - let time_to_community: BlockNumberFor = time_to_auction + - ::AuctionOpeningDuration::get() + - ::AuctionClosingDuration::get(); - let time_to_remainder: BlockNumberFor = time_to_community + ::CommunityFundingDuration::get(); - let time_to_finish: BlockNumberFor = time_to_remainder + ::RemainderFundingDuration::get(); - let mut inst = mutex_inst.lock().await; - let now = inst.current_block(); - drop(inst); - - match test_project_params.expected_state { - ProjectStatus::Application => { - let notify = Arc::new(Notify::new()); - block_orchestrator - .add_awaiting_project(now + time_to_finish - time_to_new_project, notify.clone()) - .await; - // Wait for the notification that our desired block was reached to continue - notify.notified().await; - async_create_new_project(mutex_inst.clone(), test_project_params.metadata, test_project_params.issuer) - .await - }, - ProjectStatus::EvaluationRound => { - let notify = Arc::new(Notify::new()); - block_orchestrator - .add_awaiting_project(now + time_to_finish - time_to_evaluation, notify.clone()) - .await; - // Wait for the notification that our desired block was reached to continue - notify.notified().await; - async_create_evaluating_project( - mutex_inst.clone(), - test_project_params.metadata, - test_project_params.issuer, - ) - .await - }, - ProjectStatus::AuctionOpening | ProjectStatus::AuctionClosing => { - let notify = Arc::new(Notify::new()); - block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_auction, notify.clone()).await; - // Wait for the notification that our desired block was reached to continue - notify.notified().await; - async_create_auctioning_project( - mutex_inst.clone(), - block_orchestrator.clone(), - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - ) - .await - }, - ProjectStatus::CommunityRound => { - let notify = Arc::new(Notify::new()); - block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_community, notify.clone()).await; - // Wait for the notification that our desired block was reached to continue - notify.notified().await; - async_create_community_contributing_project( - mutex_inst.clone(), - block_orchestrator.clone(), - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - ) - .map(|(project_id, _)| project_id) - .await - }, - ProjectStatus::RemainderRound => { - let notify = Arc::new(Notify::new()); - block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_remainder, notify.clone()).await; - // Wait for the notification that our desired block was reached to continue - notify.notified().await; - async_create_remainder_contributing_project( - mutex_inst.clone(), - block_orchestrator.clone(), - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - test_project_params.community_contributions, - ) - .map(|(project_id, _)| project_id) - .await - }, - ProjectStatus::FundingSuccessful => { - let notify = Arc::new(Notify::new()); - block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_finish, notify.clone()).await; - // Wait for the notification that our desired block was reached to continue - notify.notified().await; - async_create_finished_project( - mutex_inst.clone(), - block_orchestrator.clone(), - test_project_params.metadata, - test_project_params.issuer, - test_project_params.evaluations, - test_project_params.bids, - test_project_params.community_contributions, - test_project_params.remainder_contributions, - ) - .await - }, - _ => unimplemented!("Unsupported project creation in that status"), - } - } - - pub fn create_multiple_projects_at< - T: Config + pallet_balances::Config>, - AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize> + 'static + 'static, - RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, - >( - instantiator: Instantiator, - projects: Vec>, - ) -> (Vec, Instantiator) { - use tokio::runtime::Builder; - let tokio_runtime = Builder::new_current_thread().enable_all().build().unwrap(); - let local = tokio::task::LocalSet::new(); - let execution = local.run_until(async move { - let block_orchestrator = Arc::new(BlockOrchestrator::new()); - let mutex_inst = Arc::new(Mutex::new(instantiator)); - - let project_futures = projects.into_iter().map(|project| { - let block_orchestrator = block_orchestrator.clone(); - let mutex_inst = mutex_inst.clone(); - tokio::task::spawn_local(async { - async_create_project_at(mutex_inst, block_orchestrator, project).await - }) - }); - - // Wait for all project creation tasks to complete - let joined_project_futures = futures::future::join_all(project_futures); - let controller_handle = - tokio::task::spawn_local(block_controller(block_orchestrator.clone(), mutex_inst.clone())); - let projects = joined_project_futures.await; - - // Now that all projects have been set up, signal the block_controller to stop - block_orchestrator.should_continue.store(false, Ordering::SeqCst); - - // Wait for the block controller to finish - controller_handle.await.unwrap(); - - let inst = Arc::try_unwrap(mutex_inst).unwrap_or_else(|_| panic!("mutex in use")).into_inner(); - let project_ids = projects.into_iter().map(|project| project.unwrap()).collect_vec(); - - (project_ids, inst) - }); - tokio_runtime.block_on(execution) - } -} - -pub trait Accounts { - type Account; - - fn accounts(&self) -> Vec; -} - -pub enum MergeOperation { - Add, - Subtract, -} -pub trait AccountMerge: Accounts + Sized { - /// The inner type of the Vec implementing this Trait. - type Inner; - /// Merge accounts in the list based on the operation. - fn merge_accounts(&self, ops: MergeOperation) -> Self; - /// Subtract amount of the matching accounts in the other list from the current list. - /// If the account is not present in the current list, it is ignored. - fn subtract_accounts(&self, other_list: Self) -> Self; - - fn sum_accounts(&self, other_list: Self) -> Self; -} - -pub trait Deposits { - fn existential_deposits(&self) -> Vec>; -} - -impl Deposits for Vec> { - fn existential_deposits(&self) -> Vec> { - self.iter() - .map(|x| UserToPLMCBalance::new(x.clone(), ::ExistentialDeposit::get())) - .collect::>() - } -} -#[derive(Clone, PartialEq, Debug)] -pub struct UserToPLMCBalance { - pub account: AccountIdOf, - pub plmc_amount: BalanceOf, -} -impl UserToPLMCBalance { - pub fn new(account: AccountIdOf, plmc_amount: BalanceOf) -> Self { - Self { account, plmc_amount } - } -} -impl Accounts for Vec> { - type Account = AccountIdOf; - - fn accounts(&self) -> Vec { - let mut btree = BTreeSet::new(); - for UserToPLMCBalance { account, plmc_amount: _ } in self.iter() { - btree.insert(account.clone()); - } - btree.into_iter().collect_vec() - } -} -impl From<(AccountIdOf, BalanceOf)> for UserToPLMCBalance { - fn from((account, plmc_amount): (AccountIdOf, BalanceOf)) -> Self { - UserToPLMCBalance::::new(account, plmc_amount) - } -} - -impl AccountMerge for Vec> { - type Inner = UserToPLMCBalance; - - fn merge_accounts(&self, ops: MergeOperation) -> Self { - let mut btree = BTreeMap::new(); - for UserToPLMCBalance { account, plmc_amount } in self.iter() { - btree - .entry(account.clone()) - .and_modify(|e: &mut BalanceOf| { - *e = match ops { - MergeOperation::Add => e.saturating_add(*plmc_amount), - MergeOperation::Subtract => e.saturating_sub(*plmc_amount), - } - }) - .or_insert(*plmc_amount); - } - btree.into_iter().map(|(account, plmc_amount)| UserToPLMCBalance::new(account, plmc_amount)).collect() - } - - fn subtract_accounts(&self, other_list: Self) -> Self { - let current_accounts = self.accounts(); - let filtered_list = other_list.into_iter().filter(|x| current_accounts.contains(&x.account)).collect_vec(); - let mut new_list = self.clone(); - new_list.extend(filtered_list); - new_list.merge_accounts(MergeOperation::Subtract) - } - - fn sum_accounts(&self, mut other_list: Self) -> Self { - let mut output = self.clone(); - output.append(&mut other_list); - output.merge_accounts(MergeOperation::Add) - } -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "std", - serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) -)] -pub struct UserToUSDBalance { - pub account: AccountIdOf, - pub usd_amount: BalanceOf, -} -impl UserToUSDBalance { - pub fn new(account: AccountIdOf, usd_amount: BalanceOf) -> Self { - Self { account, usd_amount } - } -} -impl From<(AccountIdOf, BalanceOf)> for UserToUSDBalance { - fn from((account, usd_amount): (AccountIdOf, BalanceOf)) -> Self { - UserToUSDBalance::::new(account, usd_amount) - } -} -impl Accounts for Vec> { - type Account = AccountIdOf; - - fn accounts(&self) -> Vec { - let mut btree = BTreeSet::new(); - for UserToUSDBalance { account, usd_amount: _ } in self { - btree.insert(account.clone()); - } - btree.into_iter().collect_vec() - } -} -impl AccountMerge for Vec> { - type Inner = UserToUSDBalance; - - fn merge_accounts(&self, ops: MergeOperation) -> Self { - let mut btree = BTreeMap::new(); - for UserToUSDBalance { account, usd_amount } in self.iter() { - btree - .entry(account.clone()) - .and_modify(|e: &mut BalanceOf| { - *e = match ops { - MergeOperation::Add => e.saturating_add(*usd_amount), - MergeOperation::Subtract => e.saturating_sub(*usd_amount), - } - }) - .or_insert(*usd_amount); - } - btree.into_iter().map(|(account, usd_amount)| UserToUSDBalance::new(account, usd_amount)).collect() - } - - fn subtract_accounts(&self, other_list: Self) -> Self { - let current_accounts = self.accounts(); - let filtered_list = other_list.into_iter().filter(|x| current_accounts.contains(&x.account)).collect_vec(); - let mut new_list = self.clone(); - new_list.extend(filtered_list); - new_list.merge_accounts(MergeOperation::Subtract) - } - - fn sum_accounts(&self, mut other_list: Self) -> Self { - let mut output = self.clone(); - output.append(&mut other_list); - output.merge_accounts(MergeOperation::Add) - } -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct UserToForeignAssets { - pub account: AccountIdOf, - pub asset_amount: BalanceOf, - pub asset_id: AssetIdOf, -} -impl UserToForeignAssets { - pub fn new(account: AccountIdOf, asset_amount: BalanceOf, asset_id: AssetIdOf) -> Self { - Self { account, asset_amount, asset_id } - } -} -impl From<(AccountIdOf, BalanceOf, AssetIdOf)> for UserToForeignAssets { - fn from((account, asset_amount, asset_id): (AccountIdOf, BalanceOf, AssetIdOf)) -> Self { - UserToForeignAssets::::new(account, asset_amount, asset_id) - } -} -impl From<(AccountIdOf, BalanceOf)> for UserToForeignAssets { - fn from((account, asset_amount): (AccountIdOf, BalanceOf)) -> Self { - UserToForeignAssets::::new(account, asset_amount, AcceptedFundingAsset::USDT.to_assethub_id()) - } -} -impl Accounts for Vec> { - type Account = AccountIdOf; - - fn accounts(&self) -> Vec { - let mut btree = BTreeSet::new(); - for UserToForeignAssets { account, .. } in self.iter() { - btree.insert(account.clone()); - } - btree.into_iter().collect_vec() - } -} -impl AccountMerge for Vec> { - type Inner = UserToForeignAssets; - - fn merge_accounts(&self, ops: MergeOperation) -> Self { - let mut btree = BTreeMap::new(); - for UserToForeignAssets { account, asset_amount, asset_id } in self.iter() { - btree - .entry(account.clone()) - .and_modify(|e: &mut (BalanceOf, u32)| { - e.0 = match ops { - MergeOperation::Add => e.0.saturating_add(*asset_amount), - MergeOperation::Subtract => e.0.saturating_sub(*asset_amount), - } - }) - .or_insert((*asset_amount, *asset_id)); - } - btree.into_iter().map(|(account, info)| UserToForeignAssets::new(account, info.0, info.1)).collect() - } - - fn subtract_accounts(&self, other_list: Self) -> Self { - let current_accounts = self.accounts(); - let filtered_list = other_list.into_iter().filter(|x| current_accounts.contains(&x.account)).collect_vec(); - let mut new_list = self.clone(); - new_list.extend(filtered_list); - new_list.merge_accounts(MergeOperation::Subtract) - } - - fn sum_accounts(&self, mut other_list: Self) -> Self { - let mut output = self.clone(); - output.append(&mut other_list); - output.merge_accounts(MergeOperation::Add) - } -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "std", - serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) -)] -pub struct BidParams { - pub bidder: AccountIdOf, - pub amount: BalanceOf, - pub multiplier: MultiplierOf, - pub asset: AcceptedFundingAsset, -} -impl BidParams { - pub fn new(bidder: AccountIdOf, amount: BalanceOf, multiplier: u8, asset: AcceptedFundingAsset) -> Self { - Self { bidder, amount, multiplier: multiplier.try_into().map_err(|_| ()).unwrap(), asset } - } - - pub fn new_with_defaults(bidder: AccountIdOf, amount: BalanceOf) -> Self { - Self { - bidder, - amount, - multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), - asset: AcceptedFundingAsset::USDT, - } - } -} -impl From<(AccountIdOf, BalanceOf)> for BidParams { - fn from((bidder, amount): (AccountIdOf, BalanceOf)) -> Self { - Self { - bidder, - amount, - multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), - asset: AcceptedFundingAsset::USDT, - } - } -} -impl From<(AccountIdOf, BalanceOf, u8)> for BidParams { - fn from((bidder, amount, multiplier): (AccountIdOf, BalanceOf, u8)) -> Self { - Self { - bidder, - amount, - multiplier: multiplier.try_into().unwrap_or_else(|_| panic!("Failed to create multiplier")), - asset: AcceptedFundingAsset::USDT, - } - } -} -impl From<(AccountIdOf, BalanceOf, u8, AcceptedFundingAsset)> for BidParams { - fn from((bidder, amount, multiplier, asset): (AccountIdOf, BalanceOf, u8, AcceptedFundingAsset)) -> Self { - Self { - bidder, - amount, - multiplier: multiplier.try_into().unwrap_or_else(|_| panic!("Failed to create multiplier")), - asset, - } - } -} - -impl Accounts for Vec> { - type Account = AccountIdOf; - - fn accounts(&self) -> Vec { - let mut btree = BTreeSet::new(); - for BidParams { bidder, .. } in self { - btree.insert(bidder.clone()); - } - btree.into_iter().collect_vec() - } -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "std", - serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) -)] -pub struct ContributionParams { - pub contributor: AccountIdOf, - pub amount: BalanceOf, - pub multiplier: MultiplierOf, - pub asset: AcceptedFundingAsset, -} -impl ContributionParams { - pub fn new(contributor: AccountIdOf, amount: BalanceOf, multiplier: u8, asset: AcceptedFundingAsset) -> Self { - Self { contributor, amount, multiplier: multiplier.try_into().map_err(|_| ()).unwrap(), asset } - } - - pub fn new_with_defaults(contributor: AccountIdOf, amount: BalanceOf) -> Self { - Self { - contributor, - amount, - multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), - asset: AcceptedFundingAsset::USDT, - } - } -} -impl From<(AccountIdOf, BalanceOf)> for ContributionParams { - fn from((contributor, amount): (AccountIdOf, BalanceOf)) -> Self { - Self { - contributor, - amount, - multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), - asset: AcceptedFundingAsset::USDT, - } - } -} -impl From<(AccountIdOf, BalanceOf, MultiplierOf)> for ContributionParams { - fn from((contributor, amount, multiplier): (AccountIdOf, BalanceOf, MultiplierOf)) -> Self { - Self { contributor, amount, multiplier, asset: AcceptedFundingAsset::USDT } - } -} -impl From<(AccountIdOf, BalanceOf, MultiplierOf, AcceptedFundingAsset)> for ContributionParams { - fn from( - (contributor, amount, multiplier, asset): (AccountIdOf, BalanceOf, MultiplierOf, AcceptedFundingAsset), - ) -> Self { - Self { contributor, amount, multiplier, asset } - } -} - -impl Accounts for Vec> { - type Account = AccountIdOf; - - fn accounts(&self) -> Vec { - let mut btree = BTreeSet::new(); - for ContributionParams { contributor, .. } in self.iter() { - btree.insert(contributor.clone()); - } - btree.into_iter().collect_vec() - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct BidInfoFilter { - pub id: Option, - pub project_id: Option, - pub bidder: Option>, - pub status: Option>>, - pub original_ct_amount: Option>, - pub original_ct_usd_price: Option>, - pub final_ct_amount: Option>, - pub final_ct_usd_price: Option>, - pub funding_asset: Option, - pub funding_asset_amount_locked: Option>, - pub multiplier: Option>, - pub plmc_bond: Option>, - pub when: Option>, -} -impl BidInfoFilter { - pub(crate) fn matches_bid(&self, bid: &BidInfoOf) -> bool { - if self.id.is_some() && self.id.unwrap() != bid.id { - return false; - } - if self.project_id.is_some() && self.project_id.unwrap() != bid.project_id { - return false; - } - if self.bidder.is_some() && self.bidder.clone().unwrap() != bid.bidder.clone() { - return false; - } - if self.status.is_some() && self.status.as_ref().unwrap() != &bid.status { - return false; - } - if self.original_ct_amount.is_some() && self.original_ct_amount.unwrap() != bid.original_ct_amount { - return false; - } - if self.original_ct_usd_price.is_some() && self.original_ct_usd_price.unwrap() != bid.original_ct_usd_price { - return false; - } - if self.final_ct_amount.is_some() && self.final_ct_amount.unwrap() != bid.final_ct_amount { - return false; - } - if self.final_ct_usd_price.is_some() && self.final_ct_usd_price.unwrap() != bid.final_ct_usd_price { - return false; - } - if self.funding_asset.is_some() && self.funding_asset.unwrap() != bid.funding_asset { - return false; - } - if self.funding_asset_amount_locked.is_some() && - self.funding_asset_amount_locked.unwrap() != bid.funding_asset_amount_locked - { - return false; - } - if self.multiplier.is_some() && self.multiplier.unwrap() != bid.multiplier { - return false; - } - if self.plmc_bond.is_some() && self.plmc_bond.unwrap() != bid.plmc_bond { - return false; - } - if self.when.is_some() && self.when.unwrap() != bid.when { - return false; - } - - true - } -} -impl Default for BidInfoFilter { - fn default() -> Self { - BidInfoFilter:: { - id: None, - project_id: None, - bidder: None, - status: None, - original_ct_amount: None, - original_ct_usd_price: None, - final_ct_amount: None, - final_ct_usd_price: None, - funding_asset: None, - funding_asset_amount_locked: None, - multiplier: None, - plmc_bond: None, - when: None, - } - } -} - -pub mod testing_macros { - - #[macro_export] - /// Example: - /// ``` - /// use pallet_funding::assert_close_enough; - /// use sp_arithmetic::Perquintill; - /// - /// let real = 98u64; - /// let desired = 100u64; - /// assert_close_enough!(real, desired, Perquintill::from_float(0.98)); - /// // This would fail - /// // assert_close_enough!(real, desired, Perquintill::from_float(0.99)); - /// ``` - macro_rules! assert_close_enough { - // Match when a message is provided - ($real:expr, $desired:expr, $min_percentage:expr, $msg:expr) => { - let actual_percentage; - if $real <= $desired { - actual_percentage = Perquintill::from_rational($real, $desired); - } else { - actual_percentage = Perquintill::from_rational($desired, $real); - } - assert!(actual_percentage >= $min_percentage, $msg); - }; - // Match when no message is provided - ($real:expr, $desired:expr, $min_percentage:expr) => { - let actual_percentage; - if $real <= $desired { - actual_percentage = Perquintill::from_rational($real, $desired); - } else { - actual_percentage = Perquintill::from_rational($desired, $real); - } - assert!( - actual_percentage >= $min_percentage, - "Actual percentage too low for the set minimum: {:?} < {:?} for {:?} and {:?}", - actual_percentage, - $min_percentage, - $real, - $desired - ); - }; - } - - #[macro_export] - macro_rules! call_and_is_ok { - ($inst: expr, $( $call: expr ),* ) => { - $inst.execute(|| { - $( - let result = $call; - assert!(result.is_ok(), "Call failed: {:?}", result); - )* - }) - }; - } - #[macro_export] - macro_rules! find_event { - ($runtime:ty, $pattern:pat, $($field_name:ident == $field_value:expr),+) => { - { - let events = frame_system::Pallet::<$runtime>::events(); - events.iter().find_map(|event_record| { - let runtime_event = event_record.event.clone(); - let runtime_event = <<$runtime as crate::Config>::RuntimeEvent>::from(runtime_event); - if let Ok(funding_event) = TryInto::>::try_into(runtime_event) { - if let $pattern = funding_event { - let mut is_match = true; - $( - is_match &= $field_name == $field_value; - )+ - if is_match { - return Some(funding_event.clone()); - } - } - None - } else { - None - } - }) - } - }; -} - - #[macro_export] - macro_rules! extract_from_event { - ($env: expr, $pattern:pat, $field:ident) => { - $env.execute(|| { - let events = System::events(); - - events.iter().find_map(|event_record| { - if let frame_system::EventRecord { event: RuntimeEvent::PolimecFunding($pattern), .. } = - event_record - { - Some($field.clone()) - } else { - None - } - }) - }) - }; - } - - #[macro_export] - macro_rules! define_names { - ($($name:ident: $id:expr, $label:expr);* $(;)?) => { - $( - pub const $name: AccountId = $id; - )* - - pub fn names() -> std::collections::HashMap { - let mut names = std::collections::HashMap::new(); - $( - names.insert($name, $label); - )* - names - } - }; - } -} diff --git a/pallets/funding/src/instantiator/async_features.rs b/pallets/funding/src/instantiator/async_features.rs new file mode 100644 index 000000000..2b70cfdb5 --- /dev/null +++ b/pallets/funding/src/instantiator/async_features.rs @@ -0,0 +1,967 @@ +use super::*; +use assert_matches2::assert_matches; +use futures::FutureExt; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, + }, + time::Duration, +}; +use tokio::{ + sync::{Mutex, Notify}, + time::sleep, +}; + +pub struct BlockOrchestrator { + pub current_block: Arc, + // used for resuming execution of a project that is waiting for a certain block to be reached + pub awaiting_projects: Mutex, Vec>>>, + pub should_continue: Arc, + pub instantiator_phantom: PhantomData<(T, AllPalletsWithoutSystem, RuntimeEvent)>, +} +pub async fn block_controller< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + block_orchestrator: Arc>, + instantiator: Arc>>, +) { + loop { + if !block_orchestrator.continue_running() { + break; + } + + let maybe_target_reached = block_orchestrator.advance_to_next_target(instantiator.clone()).await; + + if let Some(target_reached) = maybe_target_reached { + block_orchestrator.execute_callbacks(target_reached).await; + } + // leaves some time for the projects to submit their targets to the orchestrator + sleep(Duration::from_millis(100)).await; + } +} + +impl< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, + > Default for BlockOrchestrator +{ + fn default() -> Self { + Self::new() + } +} + +impl< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, + > BlockOrchestrator +{ + pub fn new() -> Self { + BlockOrchestrator:: { + current_block: Arc::new(AtomicU32::new(0)), + awaiting_projects: Mutex::new(HashMap::new()), + should_continue: Arc::new(AtomicBool::new(true)), + instantiator_phantom: PhantomData, + } + } + + pub async fn add_awaiting_project(&self, block_number: BlockNumberFor, notify: Arc) { + let mut awaiting_projects = self.awaiting_projects.lock().await; + awaiting_projects.entry(block_number).or_default().push(notify); + drop(awaiting_projects); + } + + pub async fn advance_to_next_target( + &self, + instantiator: Arc>>, + ) -> Option> { + let mut inst = instantiator.lock().await; + let now: u32 = inst.current_block().try_into().unwrap_or_else(|_| panic!("Block number should fit into u32")); + self.current_block.store(now, Ordering::SeqCst); + + let awaiting_projects = self.awaiting_projects.lock().await; + + if let Some(&next_block) = awaiting_projects.keys().min() { + drop(awaiting_projects); + + while self.get_current_block() < next_block { + inst.advance_time(One::one()).unwrap(); + let current_block: u32 = + self.get_current_block().try_into().unwrap_or_else(|_| panic!("Block number should fit into u32")); + self.current_block.store(current_block + 1u32, Ordering::SeqCst); + } + Some(next_block) + } else { + None + } + } + + pub async fn execute_callbacks(&self, block_number: BlockNumberFor) { + let mut awaiting_projects = self.awaiting_projects.lock().await; + if let Some(notifies) = awaiting_projects.remove(&block_number) { + for notify in notifies { + notify.notify_one(); + } + } + } + + pub async fn is_empty(&self) -> bool { + self.awaiting_projects.lock().await.is_empty() + } + + // Method to check if the loop should continue + pub fn continue_running(&self) -> bool { + self.should_continue.load(Ordering::SeqCst) + } + + // Method to get the current block number + pub fn get_current_block(&self) -> BlockNumberFor { + self.current_block.load(Ordering::SeqCst).into() + } +} + +// async instantiations for parallel testing +pub async fn async_create_new_project< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, +) -> ProjectId { + let mut inst = instantiator.lock().await; + + let now = inst.current_block(); + // One ED for the issuer, one for the escrow account + inst.mint_plmc_to(vec![UserToPLMCBalance::new( + issuer.clone(), + Instantiator::::get_ed() * 2u64.into(), + )]); + inst.execute(|| { + crate::Pallet::::do_create_project( + &issuer.clone(), + project_metadata.clone(), + generate_did_from_account(issuer.clone()), + ) + .unwrap(); + let last_project_metadata = ProjectsMetadata::::iter().last().unwrap(); + log::trace!("Last project metadata: {:?}", last_project_metadata); + }); + + let created_project_id = inst.execute(|| NextProjectId::::get().saturating_sub(One::one())); + inst.creation_assertions(created_project_id, project_metadata, now); + created_project_id +} + +pub async fn async_create_evaluating_project< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, +) -> ProjectId { + let project_id = async_create_new_project(instantiator.clone(), project_metadata, issuer.clone()).await; + + let mut inst = instantiator.lock().await; + + inst.start_evaluation(project_id, issuer).unwrap(); + project_id +} + +pub async fn async_start_auction< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_id: ProjectId, + caller: AccountIdOf, +) -> Result<(), DispatchError> { + let mut inst = instantiator.lock().await; + + let project_details = inst.get_project_details(project_id); + + if project_details.status == ProjectStatus::EvaluationRound { + let update_block = inst.get_update_block(project_id, &UpdateType::EvaluationEnd).unwrap(); + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(update_block + 1u32.into(), notify.clone()).await; + + // Wait for the notification that our desired block was reached to continue + drop(inst); + + notify.notified().await; + + inst = instantiator.lock().await; + }; + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); + + inst.execute(|| crate::Pallet::::do_auction_opening(caller.clone(), project_id).unwrap()); + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionOpening); + + Ok(()) +} + +pub async fn async_create_auctioning_project< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, +) -> ProjectId { + let project_id = + async_create_evaluating_project(instantiator.clone(), project_metadata.clone(), issuer.clone()).await; + + let mut inst = instantiator.lock().await; + + let evaluators = evaluations.accounts(); + let prev_supply = inst.get_plmc_total_supply(); + let prev_plmc_balances = inst.get_free_plmc_balances_for(evaluators.clone()); + + let plmc_eval_deposits: Vec> = + Instantiator::::calculate_evaluation_plmc_spent(evaluations.clone()); + let plmc_existential_deposits: Vec> = evaluators.existential_deposits(); + + let expected_remaining_plmc: Vec> = + Instantiator::::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + inst.mint_plmc_to(plmc_eval_deposits.clone()); + inst.mint_plmc_to(plmc_existential_deposits.clone()); + + inst.evaluate_for_users(project_id, evaluations).unwrap(); + + let expected_evaluator_balances = + Instantiator::::sum_balance_mappings(vec![ + plmc_eval_deposits.clone(), + plmc_existential_deposits.clone(), + ]); + + let expected_total_supply = prev_supply + expected_evaluator_balances; + + inst.evaluation_assertions(project_id, expected_remaining_plmc, plmc_eval_deposits, expected_total_supply); + + drop(inst); + + async_start_auction(instantiator.clone(), block_orchestrator, project_id, issuer).await.unwrap(); + + inst = instantiator.lock().await; + let plmc_for_bids = + Instantiator::::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata.clone(), + None + ); + let plmc_existential_deposits: Vec> = bids.accounts().existential_deposits(); + let usdt_for_bids = + Instantiator::::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata, + None + ); + + inst.mint_plmc_to(plmc_for_bids.clone()); + inst.mint_plmc_to(plmc_existential_deposits.clone()); + inst.mint_foreign_asset_to(usdt_for_bids.clone()); + + inst.bid_for_users(project_id, bids).unwrap(); + drop(inst); + + project_id +} + +pub async fn async_start_community_funding< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_id: ProjectId, +) -> Result<(), DispatchError> { + let mut inst = instantiator.lock().await; + + let update_block = inst.get_update_block(project_id, &UpdateType::AuctionClosingStart).unwrap(); + let closing_start = update_block + 1u32.into(); + + let notify = Arc::new(Notify::new()); + + block_orchestrator.add_awaiting_project(closing_start, notify.clone()).await; + + // Wait for the notification that our desired block was reached to continue + + drop(inst); + + notify.notified().await; + + inst = instantiator.lock().await; + let update_block = inst.get_update_block(project_id, &UpdateType::CommunityFundingStart).unwrap(); + let community_start = update_block + 1u32.into(); + + let notify = Arc::new(Notify::new()); + + block_orchestrator.add_awaiting_project(community_start, notify.clone()).await; + + drop(inst); + + notify.notified().await; + + inst = instantiator.lock().await; + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::CommunityRound); + + Ok(()) +} + +pub async fn async_create_community_contributing_project< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, +) -> (ProjectId, Vec>) { + if bids.is_empty() { + panic!("Cannot start community funding without bids") + } + + let project_id = async_create_auctioning_project( + instantiator.clone(), + block_orchestrator.clone(), + project_metadata.clone(), + issuer, + evaluations.clone(), + vec![], + ) + .await; + + let mut inst = instantiator.lock().await; + + let bidders = bids.accounts(); + let asset_id = bids[0].asset.to_assethub_id(); + let prev_plmc_balances = inst.get_free_plmc_balances_for(bidders.clone()); + let prev_funding_asset_balances = inst.get_free_foreign_asset_balances_for(asset_id, bidders.clone()); + let plmc_evaluation_deposits: Vec> = + Instantiator::::calculate_evaluation_plmc_spent(evaluations); + let plmc_bid_deposits: Vec> = + Instantiator::::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata.clone(), + None + ); + let participation_usable_evaluation_deposits = plmc_evaluation_deposits + .into_iter() + .map(|mut x| { + x.plmc_amount = x.plmc_amount.saturating_sub(::EvaluatorSlash::get() * x.plmc_amount); + x + }) + .collect::>>(); + let necessary_plmc_mint = Instantiator::::generic_map_operation( + vec![plmc_bid_deposits.clone(), participation_usable_evaluation_deposits], + MergeOperation::Subtract, + ); + let total_plmc_participation_locked = plmc_bid_deposits; + let plmc_existential_deposits: Vec> = bidders.existential_deposits(); + let funding_asset_deposits = + Instantiator::::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata.clone(), + None + ); + + let bidder_balances = Instantiator::::sum_balance_mappings(vec![ + necessary_plmc_mint.clone(), + plmc_existential_deposits.clone(), + ]); + + let expected_free_plmc_balances = Instantiator::::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + let prev_supply = inst.get_plmc_total_supply(); + let post_supply = prev_supply + bidder_balances; + + inst.mint_plmc_to(necessary_plmc_mint.clone()); + inst.mint_plmc_to(plmc_existential_deposits.clone()); + inst.mint_foreign_asset_to(funding_asset_deposits.clone()); + + inst.bid_for_users(project_id, bids.clone()).unwrap(); + + inst.do_reserved_plmc_assertions( + total_plmc_participation_locked.merge_accounts(MergeOperation::Add), + HoldReason::Participation(project_id).into(), + ); + inst.do_bid_transferred_foreign_asset_assertions( + funding_asset_deposits.merge_accounts(MergeOperation::Add), + project_id, + ); + inst.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); + inst.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); + assert_eq!(inst.get_plmc_total_supply(), post_supply); + + drop(inst); + async_start_community_funding(instantiator.clone(), block_orchestrator, project_id).await.unwrap(); + let mut inst = instantiator.lock().await; + + let _weighted_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); + let accepted_bids = Instantiator::::filter_bids_after_auction( + bids, + project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size, + ); + let bid_expectations = accepted_bids + .iter() + .map(|bid| BidInfoFilter:: { + bidder: Some(bid.bidder.clone()), + final_ct_amount: Some(bid.amount), + ..Default::default() + }) + .collect_vec(); + + let total_ct_sold = accepted_bids.iter().map(|bid| bid.amount).fold(Zero::zero(), |acc, item| item + acc); + + inst.finalized_bids_assertions(project_id, bid_expectations, total_ct_sold); + + (project_id, accepted_bids) +} + +pub async fn async_start_remainder_or_end_funding< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_id: ProjectId, +) -> Result<(), DispatchError> { + let mut inst = instantiator.lock().await; + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::CommunityRound); + + let update_block = inst.get_update_block(project_id, &UpdateType::RemainderFundingStart).unwrap(); + let remainder_start = update_block + 1u32.into(); + + let notify = Arc::new(Notify::new()); + + block_orchestrator.add_awaiting_project(remainder_start, notify.clone()).await; + + // Wait for the notification that our desired block was reached to continue + + drop(inst); + + notify.notified().await; + + let mut inst = instantiator.lock().await; + + assert_matches!( + inst.get_project_details(project_id).status, + (ProjectStatus::RemainderRound | ProjectStatus::FundingSuccessful) + ); + Ok(()) +} + +pub async fn async_create_remainder_contributing_project< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, + contributions: Vec>, +) -> (ProjectId, Vec>) { + let (project_id, accepted_bids) = async_create_community_contributing_project( + instantiator.clone(), + block_orchestrator.clone(), + project_metadata, + issuer, + evaluations.clone(), + bids, + ) + .await; + + if contributions.is_empty() { + async_start_remainder_or_end_funding(instantiator.clone(), block_orchestrator.clone(), project_id) + .await + .unwrap(); + return (project_id, accepted_bids); + } + + let mut inst = instantiator.lock().await; + + let ct_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); + let contributors = contributions.accounts(); + let asset_id = contributions[0].asset.to_assethub_id(); + let prev_plmc_balances = inst.get_free_plmc_balances_for(contributors.clone()); + let prev_funding_asset_balances = inst.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); + + let plmc_evaluation_deposits = + Instantiator::::calculate_evaluation_plmc_spent(evaluations); + let plmc_bid_deposits = + Instantiator::::calculate_auction_plmc_charged_with_given_price( + &accepted_bids, + ct_price, + ); + + let plmc_contribution_deposits = + Instantiator::::calculate_contributed_plmc_spent( + contributions.clone(), + ct_price, + ); + + let necessary_plmc_mint = Instantiator::::generic_map_operation( + vec![plmc_contribution_deposits.clone(), plmc_evaluation_deposits], + MergeOperation::Subtract, + ); + let total_plmc_participation_locked = + Instantiator::::generic_map_operation( + vec![plmc_bid_deposits, plmc_contribution_deposits], + MergeOperation::Add, + ); + let plmc_existential_deposits = contributors.existential_deposits(); + + let funding_asset_deposits = + Instantiator::::calculate_contributed_funding_asset_spent( + contributions.clone(), + ct_price, + ); + let contributor_balances = Instantiator::::sum_balance_mappings(vec![ + necessary_plmc_mint.clone(), + plmc_existential_deposits.clone(), + ]); + + let expected_free_plmc_balances = Instantiator::::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + let prev_supply = inst.get_plmc_total_supply(); + let post_supply = prev_supply + contributor_balances; + + inst.mint_plmc_to(necessary_plmc_mint.clone()); + inst.mint_plmc_to(plmc_existential_deposits.clone()); + inst.mint_foreign_asset_to(funding_asset_deposits.clone()); + + inst.contribute_for_users(project_id, contributions).expect("Contributing should work"); + + inst.do_reserved_plmc_assertions( + total_plmc_participation_locked.merge_accounts(MergeOperation::Add), + HoldReason::Participation(project_id).into(), + ); + inst.do_contribution_transferred_foreign_asset_assertions( + funding_asset_deposits.merge_accounts(MergeOperation::Add), + project_id, + ); + inst.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); + inst.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); + assert_eq!(inst.get_plmc_total_supply(), post_supply); + drop(inst); + async_start_remainder_or_end_funding(instantiator.clone(), block_orchestrator.clone(), project_id).await.unwrap(); + (project_id, accepted_bids) +} + +pub async fn async_finish_funding< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_id: ProjectId, +) -> Result<(), DispatchError> { + let mut inst = instantiator.lock().await; + let update_block = inst.get_update_block(project_id, &UpdateType::FundingEnd).unwrap(); + + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(update_block + 1u32.into(), notify.clone()).await; + Ok(()) +} + +pub async fn async_create_finished_project< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, + community_contributions: Vec>, + remainder_contributions: Vec>, +) -> ProjectId { + let (project_id, accepted_bids) = async_create_remainder_contributing_project( + instantiator.clone(), + block_orchestrator.clone(), + project_metadata.clone(), + issuer, + evaluations.clone(), + bids.clone(), + community_contributions.clone(), + ) + .await; + + let mut inst = instantiator.lock().await; + + let total_ct_sold_in_bids = bids.iter().map(|bid| bid.amount).fold(Zero::zero(), |acc, item| item + acc); + let total_ct_sold_in_community_contributions = + community_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); + let total_ct_sold_in_remainder_contributions = + remainder_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); + + let total_ct_sold = + total_ct_sold_in_bids + total_ct_sold_in_community_contributions + total_ct_sold_in_remainder_contributions; + let total_ct_available = project_metadata.total_allocation_size; + assert!( + total_ct_sold <= total_ct_available, + "Some CT buys are getting less than expected due to running out of CTs. This is ok in the runtime, but likely unexpected from the parameters of this instantiation" + ); + + match inst.get_project_details(project_id).status { + ProjectStatus::FundingSuccessful => return project_id, + ProjectStatus::RemainderRound if remainder_contributions.is_empty() => { + drop(inst); + async_finish_funding(instantiator.clone(), block_orchestrator.clone(), project_id).await.unwrap(); + return project_id; + }, + _ => {}, + }; + + let ct_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); + let contributors = remainder_contributions.accounts(); + let asset_id = remainder_contributions[0].asset.to_assethub_id(); + let prev_plmc_balances = inst.get_free_plmc_balances_for(contributors.clone()); + let prev_funding_asset_balances = inst.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); + + let plmc_evaluation_deposits = + Instantiator::::calculate_evaluation_plmc_spent(evaluations); + let plmc_bid_deposits = + Instantiator::::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &accepted_bids, + project_metadata.clone(), + None + ); + let plmc_community_contribution_deposits = + Instantiator::::calculate_contributed_plmc_spent( + community_contributions.clone(), + ct_price, + ); + let plmc_remainder_contribution_deposits = + Instantiator::::calculate_contributed_plmc_spent( + remainder_contributions.clone(), + ct_price, + ); + + let necessary_plmc_mint = Instantiator::::generic_map_operation( + vec![plmc_remainder_contribution_deposits.clone(), plmc_evaluation_deposits], + MergeOperation::Subtract, + ); + let total_plmc_participation_locked = + Instantiator::::generic_map_operation( + vec![plmc_bid_deposits, plmc_community_contribution_deposits, plmc_remainder_contribution_deposits], + MergeOperation::Add, + ); + let plmc_existential_deposits = contributors.existential_deposits(); + let funding_asset_deposits = + Instantiator::::calculate_contributed_funding_asset_spent( + remainder_contributions.clone(), + ct_price, + ); + + let contributor_balances = Instantiator::::sum_balance_mappings(vec![ + necessary_plmc_mint.clone(), + plmc_existential_deposits.clone(), + ]); + + let expected_free_plmc_balances = Instantiator::::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + let prev_supply = inst.get_plmc_total_supply(); + let post_supply = prev_supply + contributor_balances; + + inst.mint_plmc_to(necessary_plmc_mint.clone()); + inst.mint_plmc_to(plmc_existential_deposits.clone()); + inst.mint_foreign_asset_to(funding_asset_deposits.clone()); + + inst.contribute_for_users(project_id, remainder_contributions.clone()).expect("Remainder Contributing should work"); + + let merged = total_plmc_participation_locked.merge_accounts(MergeOperation::Add); + + inst.do_reserved_plmc_assertions(merged, HoldReason::Participation(project_id).into()); + + inst.do_contribution_transferred_foreign_asset_assertions( + funding_asset_deposits.merge_accounts(MergeOperation::Add), + project_id, + ); + inst.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); + inst.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); + assert_eq!(inst.get_plmc_total_supply(), post_supply); + + drop(inst); + async_finish_funding(instantiator.clone(), block_orchestrator.clone(), project_id).await.unwrap(); + let mut inst = instantiator.lock().await; + + if inst.get_project_details(project_id).status == ProjectStatus::FundingSuccessful { + // Check that remaining CTs are updated + let project_details = inst.get_project_details(project_id); + let auction_bought_tokens = + accepted_bids.iter().map(|bid| bid.amount).fold(Zero::zero(), |acc, item| item + acc); + let community_bought_tokens = + community_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); + let remainder_bought_tokens = + remainder_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); + + assert_eq!( + project_details.remaining_contribution_tokens, + project_metadata.total_allocation_size - + auction_bought_tokens - + community_bought_tokens - + remainder_bought_tokens, + "Remaining CTs are incorrect" + ); + } + + project_id +} + +pub async fn create_project_at< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Arc>>, + block_orchestrator: Arc>, + test_project_params: TestProjectParams, +) -> ProjectId { + match test_project_params.expected_state { + ProjectStatus::FundingSuccessful => + async_create_finished_project( + instantiator, + block_orchestrator, + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + test_project_params.community_contributions, + test_project_params.remainder_contributions, + ) + .await, + ProjectStatus::RemainderRound => + async_create_remainder_contributing_project( + instantiator, + block_orchestrator, + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + test_project_params.community_contributions, + ) + .map(|(project_id, _)| project_id) + .await, + ProjectStatus::CommunityRound => + async_create_community_contributing_project( + instantiator, + block_orchestrator, + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + ) + .map(|(project_id, _)| project_id) + .await, + ProjectStatus::AuctionOpening => + async_create_auctioning_project( + instantiator, + block_orchestrator, + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + ) + .await, + ProjectStatus::EvaluationRound => + async_create_evaluating_project(instantiator, test_project_params.metadata, test_project_params.issuer) + .await, + ProjectStatus::Application => + async_create_new_project(instantiator, test_project_params.metadata, test_project_params.issuer).await, + _ => panic!("unsupported project creation in that status"), + } +} + +pub async fn async_create_project_at< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + mutex_inst: Arc>>, + block_orchestrator: Arc>, + test_project_params: TestProjectParams, +) -> ProjectId { + let time_to_new_project: BlockNumberFor = Zero::zero(); + let time_to_evaluation: BlockNumberFor = time_to_new_project + Zero::zero(); + // we immediately start the auction, so we dont wait for T::AuctionInitializePeriodDuration. + let time_to_auction: BlockNumberFor = time_to_evaluation + ::EvaluationDuration::get(); + let time_to_community: BlockNumberFor = + time_to_auction + ::AuctionOpeningDuration::get() + ::AuctionClosingDuration::get(); + let time_to_remainder: BlockNumberFor = time_to_community + ::CommunityFundingDuration::get(); + let time_to_finish: BlockNumberFor = time_to_remainder + ::RemainderFundingDuration::get(); + let mut inst = mutex_inst.lock().await; + let now = inst.current_block(); + drop(inst); + + match test_project_params.expected_state { + ProjectStatus::Application => { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_new_project, notify.clone()).await; + // Wait for the notification that our desired block was reached to continue + notify.notified().await; + async_create_new_project(mutex_inst.clone(), test_project_params.metadata, test_project_params.issuer).await + }, + ProjectStatus::EvaluationRound => { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_evaluation, notify.clone()).await; + // Wait for the notification that our desired block was reached to continue + notify.notified().await; + async_create_evaluating_project( + mutex_inst.clone(), + test_project_params.metadata, + test_project_params.issuer, + ) + .await + }, + ProjectStatus::AuctionOpening | ProjectStatus::AuctionClosing => { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_auction, notify.clone()).await; + // Wait for the notification that our desired block was reached to continue + notify.notified().await; + async_create_auctioning_project( + mutex_inst.clone(), + block_orchestrator.clone(), + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + ) + .await + }, + ProjectStatus::CommunityRound => { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_community, notify.clone()).await; + // Wait for the notification that our desired block was reached to continue + notify.notified().await; + async_create_community_contributing_project( + mutex_inst.clone(), + block_orchestrator.clone(), + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + ) + .map(|(project_id, _)| project_id) + .await + }, + ProjectStatus::RemainderRound => { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_remainder, notify.clone()).await; + // Wait for the notification that our desired block was reached to continue + notify.notified().await; + async_create_remainder_contributing_project( + mutex_inst.clone(), + block_orchestrator.clone(), + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + test_project_params.community_contributions, + ) + .map(|(project_id, _)| project_id) + .await + }, + ProjectStatus::FundingSuccessful => { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(now + time_to_finish - time_to_finish, notify.clone()).await; + // Wait for the notification that our desired block was reached to continue + notify.notified().await; + async_create_finished_project( + mutex_inst.clone(), + block_orchestrator.clone(), + test_project_params.metadata, + test_project_params.issuer, + test_project_params.evaluations, + test_project_params.bids, + test_project_params.community_contributions, + test_project_params.remainder_contributions, + ) + .await + }, + _ => unimplemented!("Unsupported project creation in that status"), + } +} + +pub fn create_multiple_projects_at< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize> + 'static + 'static, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +>( + instantiator: Instantiator, + projects: Vec>, +) -> (Vec, Instantiator) { + use tokio::runtime::Builder; + let tokio_runtime = Builder::new_current_thread().enable_all().build().unwrap(); + let local = tokio::task::LocalSet::new(); + let execution = local.run_until(async move { + let block_orchestrator = Arc::new(BlockOrchestrator::new()); + let mutex_inst = Arc::new(Mutex::new(instantiator)); + + let project_futures = projects.into_iter().map(|project| { + let block_orchestrator = block_orchestrator.clone(); + let mutex_inst = mutex_inst.clone(); + tokio::task::spawn_local(async { async_create_project_at(mutex_inst, block_orchestrator, project).await }) + }); + + // Wait for all project creation tasks to complete + let joined_project_futures = futures::future::join_all(project_futures); + let controller_handle = + tokio::task::spawn_local(block_controller(block_orchestrator.clone(), mutex_inst.clone())); + let projects = joined_project_futures.await; + + // Now that all projects have been set up, signal the block_controller to stop + block_orchestrator.should_continue.store(false, Ordering::SeqCst); + + // Wait for the block controller to finish + controller_handle.await.unwrap(); + + let inst = Arc::try_unwrap(mutex_inst).unwrap_or_else(|_| panic!("mutex in use")).into_inner(); + let project_ids = projects.into_iter().map(|project| project.unwrap()).collect_vec(); + + (project_ids, inst) + }); + tokio_runtime.block_on(execution) +} diff --git a/pallets/funding/src/instantiator/calculations.rs b/pallets/funding/src/instantiator/calculations.rs new file mode 100644 index 000000000..5b16f4ef6 --- /dev/null +++ b/pallets/funding/src/instantiator/calculations.rs @@ -0,0 +1,563 @@ +use super::*; + +impl< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, + > Instantiator +{ + pub fn get_ed() -> BalanceOf { + T::ExistentialDeposit::get() + } + + pub fn get_ct_account_deposit() -> BalanceOf { + ::ContributionTokenCurrency::deposit_required(One::one()) + } + + pub fn calculate_evaluation_plmc_spent(evaluations: Vec>) -> Vec> { + let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); + let mut output = Vec::new(); + for eval in evaluations { + let usd_bond = eval.usd_amount; + let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); + output.push(UserToPLMCBalance::new(eval.account, plmc_bond)); + } + output + } + + pub fn get_actual_price_charged_for_bucketed_bids( + bids: &Vec>, + project_metadata: ProjectMetadataOf, + maybe_bucket: Option>, + ) -> Vec<(BidParams, PriceOf)> { + let mut output = Vec::new(); + let mut bucket = if let Some(bucket) = maybe_bucket { + bucket + } else { + Pallet::::create_bucket_from_metadata(&project_metadata).unwrap() + }; + for bid in bids { + let mut amount_to_bid = bid.amount; + while !amount_to_bid.is_zero() { + let bid_amount = if amount_to_bid <= bucket.amount_left { amount_to_bid } else { bucket.amount_left }; + output.push(( + BidParams { + bidder: bid.bidder.clone(), + amount: bid_amount, + multiplier: bid.multiplier, + asset: bid.asset, + }, + bucket.current_price, + )); + bucket.update(bid_amount); + amount_to_bid.saturating_reduce(bid_amount); + } + } + output + } + + pub fn calculate_auction_plmc_charged_with_given_price( + bids: &Vec>, + ct_price: PriceOf, + ) -> Vec> { + let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); + let mut output = Vec::new(); + for bid in bids { + let usd_ticket_size = ct_price.saturating_mul_int(bid.amount); + let usd_bond = bid.multiplier.calculate_bonding_requirement::(usd_ticket_size).unwrap(); + let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); + output.push(UserToPLMCBalance::new(bid.bidder.clone(), plmc_bond)); + } + output + } + + // Make sure you give it all the bids made for the project. It doesn't require a ct_price, since it will simulate the bucket prices itself + pub fn calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + bids: &Vec>, + project_metadata: ProjectMetadataOf, + maybe_bucket: Option>, + ) -> Vec> { + let mut output = Vec::new(); + let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); + + for (bid, price) in Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata, maybe_bucket) { + let usd_ticket_size = price.saturating_mul_int(bid.amount); + let usd_bond = bid.multiplier.calculate_bonding_requirement::(usd_ticket_size).unwrap(); + let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); + output.push(UserToPLMCBalance::::new(bid.bidder.clone(), plmc_bond)); + } + + output.merge_accounts(MergeOperation::Add) + } + + // WARNING: Only put bids that you are sure will be done before the random end of the closing auction + pub fn calculate_auction_plmc_returned_from_all_bids_made( + // bids in the order they were made + bids: &Vec>, + project_metadata: ProjectMetadataOf, + weighted_average_price: PriceOf, + ) -> Vec> { + let mut output = Vec::new(); + let charged_bids = Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata.clone(), None); + let grouped_by_price_bids = charged_bids.clone().into_iter().group_by(|&(_, price)| price); + let mut grouped_by_price_bids: Vec<(PriceOf, Vec>)> = grouped_by_price_bids + .into_iter() + .map(|(key, group)| (key, group.map(|(bid, _price_)| bid).collect())) + .collect(); + grouped_by_price_bids.reverse(); + + let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); + let mut remaining_cts = + project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; + + for (price_charged, bids) in grouped_by_price_bids { + for bid in bids { + let charged_usd_ticket_size = price_charged.saturating_mul_int(bid.amount); + let charged_usd_bond = + bid.multiplier.calculate_bonding_requirement::(charged_usd_ticket_size).unwrap(); + let charged_plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(charged_usd_bond); + + if remaining_cts <= Zero::zero() { + output.push(UserToPLMCBalance::new(bid.bidder, charged_plmc_bond)); + continue + } + + let bought_cts = if remaining_cts < bid.amount { remaining_cts } else { bid.amount }; + remaining_cts = remaining_cts.saturating_sub(bought_cts); + + let final_price = + if weighted_average_price > price_charged { price_charged } else { weighted_average_price }; + + let actual_usd_ticket_size = final_price.saturating_mul_int(bought_cts); + let actual_usd_bond = + bid.multiplier.calculate_bonding_requirement::(actual_usd_ticket_size).unwrap(); + let actual_plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(actual_usd_bond); + + let returned_plmc_bond = charged_plmc_bond - actual_plmc_bond; + + output.push(UserToPLMCBalance::::new(bid.bidder, returned_plmc_bond)); + } + } + + output.merge_accounts(MergeOperation::Add) + } + + pub fn calculate_auction_plmc_spent_post_wap( + bids: &Vec>, + project_metadata: ProjectMetadataOf, + weighted_average_price: PriceOf, + ) -> Vec> { + let plmc_charged = Self::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + bids, + project_metadata.clone(), + None, + ); + let plmc_returned = Self::calculate_auction_plmc_returned_from_all_bids_made( + bids, + project_metadata.clone(), + weighted_average_price, + ); + + plmc_charged.subtract_accounts(plmc_returned) + } + + pub fn calculate_auction_funding_asset_charged_with_given_price( + bids: &Vec>, + ct_price: PriceOf, + ) -> Vec> { + let mut output = Vec::new(); + for bid in bids { + let asset_price = T::PriceProvider::get_price(bid.asset.to_assethub_id()).unwrap(); + let usd_ticket_size = ct_price.saturating_mul_int(bid.amount); + let funding_asset_spent = asset_price.reciprocal().unwrap().saturating_mul_int(usd_ticket_size); + output.push(UserToForeignAssets::new(bid.bidder.clone(), funding_asset_spent, bid.asset.to_assethub_id())); + } + output + } + + // Make sure you give it all the bids made for the project. It doesn't require a ct_price, since it will simulate the bucket prices itself + pub fn calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + bids: &Vec>, + project_metadata: ProjectMetadataOf, + maybe_bucket: Option>, + ) -> Vec> { + let mut output = Vec::new(); + + for (bid, price) in Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata, maybe_bucket) { + let asset_price = T::PriceProvider::get_price(bid.asset.to_assethub_id()).unwrap(); + let usd_ticket_size = price.saturating_mul_int(bid.amount); + let funding_asset_spent = asset_price.reciprocal().unwrap().saturating_mul_int(usd_ticket_size); + output.push(UserToForeignAssets::::new( + bid.bidder.clone(), + funding_asset_spent, + bid.asset.to_assethub_id(), + )); + } + + output.merge_accounts(MergeOperation::Add) + } + + // WARNING: Only put bids that you are sure will be done before the random end of the closing auction + pub fn calculate_auction_funding_asset_returned_from_all_bids_made( + // bids in the order they were made + bids: &Vec>, + project_metadata: ProjectMetadataOf, + weighted_average_price: PriceOf, + ) -> Vec> { + let mut output = Vec::new(); + let charged_bids = Self::get_actual_price_charged_for_bucketed_bids(bids, project_metadata.clone(), None); + let grouped_by_price_bids = charged_bids.clone().into_iter().group_by(|&(_, price)| price); + let mut grouped_by_price_bids: Vec<(PriceOf, Vec>)> = grouped_by_price_bids + .into_iter() + .map(|(key, group)| (key, group.map(|(bid, _price)| bid).collect())) + .collect(); + grouped_by_price_bids.reverse(); + + let mut remaining_cts = + project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; + + for (price_charged, bids) in grouped_by_price_bids { + for bid in bids { + let funding_asset_price = T::PriceProvider::get_price(bid.asset.to_assethub_id()).unwrap(); + + let charged_usd_ticket_size = price_charged.saturating_mul_int(bid.amount); + let charged_usd_bond = + bid.multiplier.calculate_bonding_requirement::(charged_usd_ticket_size).unwrap(); + let charged_funding_asset = + funding_asset_price.reciprocal().unwrap().saturating_mul_int(charged_usd_bond); + + if remaining_cts <= Zero::zero() { + output.push(UserToForeignAssets::new( + bid.bidder, + charged_funding_asset, + bid.asset.to_assethub_id(), + )); + continue + } + + let bought_cts = if remaining_cts < bid.amount { remaining_cts } else { bid.amount }; + remaining_cts = remaining_cts.saturating_sub(bought_cts); + + let final_price = + if weighted_average_price > price_charged { price_charged } else { weighted_average_price }; + + let actual_usd_ticket_size = final_price.saturating_mul_int(bought_cts); + let actual_usd_bond = + bid.multiplier.calculate_bonding_requirement::(actual_usd_ticket_size).unwrap(); + let actual_funding_asset_spent = + funding_asset_price.reciprocal().unwrap().saturating_mul_int(actual_usd_bond); + + let returned_foreign_asset = charged_funding_asset - actual_funding_asset_spent; + + output.push(UserToForeignAssets::::new( + bid.bidder, + returned_foreign_asset, + bid.asset.to_assethub_id(), + )); + } + } + + output.merge_accounts(MergeOperation::Add) + } + + pub fn calculate_auction_funding_asset_spent_post_wap( + bids: &Vec>, + project_metadata: ProjectMetadataOf, + weighted_average_price: PriceOf, + ) -> Vec> { + let funding_asset_charged = Self::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + bids, + project_metadata.clone(), + None, + ); + let funding_asset_returned = Self::calculate_auction_funding_asset_returned_from_all_bids_made( + bids, + project_metadata.clone(), + weighted_average_price, + ); + + funding_asset_charged.subtract_accounts(funding_asset_returned) + } + + /// Filters the bids that would be rejected after the auction ends. + pub fn filter_bids_after_auction(bids: Vec>, total_cts: BalanceOf) -> Vec> { + let mut filtered_bids: Vec> = Vec::new(); + let sorted_bids = bids; + let mut total_cts_left = total_cts; + for bid in sorted_bids { + if total_cts_left >= bid.amount { + total_cts_left.saturating_reduce(bid.amount); + filtered_bids.push(bid); + } else if !total_cts_left.is_zero() { + filtered_bids.push(BidParams { + bidder: bid.bidder.clone(), + amount: total_cts_left, + multiplier: bid.multiplier, + asset: bid.asset, + }); + total_cts_left = Zero::zero(); + } + } + filtered_bids + } + + pub fn calculate_contributed_plmc_spent( + contributions: Vec>, + token_usd_price: PriceOf, + ) -> Vec> { + let plmc_price = T::PriceProvider::get_price(PLMC_FOREIGN_ID).unwrap(); + let mut output = Vec::new(); + for cont in contributions { + let usd_ticket_size = token_usd_price.saturating_mul_int(cont.amount); + let usd_bond = cont.multiplier.calculate_bonding_requirement::(usd_ticket_size).unwrap(); + let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); + output.push(UserToPLMCBalance::new(cont.contributor, plmc_bond)); + } + output + } + + pub fn calculate_total_plmc_locked_from_evaluations_and_remainder_contributions( + evaluations: Vec>, + contributions: Vec>, + price: PriceOf, + slashed: bool, + ) -> Vec> { + let evaluation_locked_plmc_amounts = Self::calculate_evaluation_plmc_spent(evaluations); + // how much new plmc would be locked without considering evaluation bonds + let theoretical_contribution_locked_plmc_amounts = Self::calculate_contributed_plmc_spent(contributions, price); + + let slash_percentage = ::EvaluatorSlash::get(); + let slashable_min_deposits = evaluation_locked_plmc_amounts + .iter() + .map(|UserToPLMCBalance { account, plmc_amount }| UserToPLMCBalance { + account: account.clone(), + plmc_amount: slash_percentage * *plmc_amount, + }) + .collect::>(); + let available_evaluation_locked_plmc_for_lock_transfer = Self::generic_map_operation( + vec![evaluation_locked_plmc_amounts.clone(), slashable_min_deposits.clone()], + MergeOperation::Subtract, + ); + + // how much new plmc was actually locked, considering already evaluation bonds used + // first. + let actual_contribution_locked_plmc_amounts = Self::generic_map_operation( + vec![theoretical_contribution_locked_plmc_amounts, available_evaluation_locked_plmc_for_lock_transfer], + MergeOperation::Subtract, + ); + let mut result = Self::generic_map_operation( + vec![evaluation_locked_plmc_amounts, actual_contribution_locked_plmc_amounts], + MergeOperation::Add, + ); + + if slashed { + result = Self::generic_map_operation(vec![result, slashable_min_deposits], MergeOperation::Subtract); + } + + result + } + + pub fn calculate_contributed_funding_asset_spent( + contributions: Vec>, + token_usd_price: PriceOf, + ) -> Vec> { + let mut output = Vec::new(); + for cont in contributions { + let asset_price = T::PriceProvider::get_price(cont.asset.to_assethub_id()).unwrap(); + let usd_ticket_size = token_usd_price.saturating_mul_int(cont.amount); + let funding_asset_spent = asset_price.reciprocal().unwrap().saturating_mul_int(usd_ticket_size); + output.push(UserToForeignAssets::new(cont.contributor, funding_asset_spent, cont.asset.to_assethub_id())); + } + output + } + + pub fn generic_map_merge_reduce( + mappings: Vec>, + key_extractor: impl Fn(&M) -> K, + initial_state: S, + merge_reduce: impl Fn(&M, S) -> S, + ) -> Vec<(K, S)> { + let mut output = BTreeMap::new(); + for mut map in mappings { + for item in map.drain(..) { + let key = key_extractor(&item); + let new_state = merge_reduce(&item, output.get(&key).cloned().unwrap_or(initial_state.clone())); + output.insert(key, new_state); + } + } + output.into_iter().collect() + } + + /// Merge the given mappings into one mapping, where the values are merged using the given + /// merge operation. + /// + /// In case of the `Add` operation, all values are Unioned, and duplicate accounts are + /// added together. + /// In case of the `Subtract` operation, all values of the first mapping are subtracted by + /// the values of the other mappings. Accounts in the other mappings that are not present + /// in the first mapping are ignored. + /// + /// # Pseudocode Example + /// List1: [(A, 10), (B, 5), (C, 5)] + /// List2: [(A, 5), (B, 5), (D, 5)] + /// + /// Add: [(A, 15), (B, 10), (C, 5), (D, 5)] + /// Subtract: [(A, 5), (B, 0), (C, 5)] + pub fn generic_map_operation< + N: AccountMerge + Extend<::Inner> + IntoIterator::Inner>, + >( + mut mappings: Vec, + ops: MergeOperation, + ) -> N { + let mut output = mappings.swap_remove(0); + output = output.merge_accounts(MergeOperation::Add); + for map in mappings { + match ops { + MergeOperation::Add => output.extend(map), + MergeOperation::Subtract => output = output.subtract_accounts(map), + } + } + output.merge_accounts(ops) + } + + pub fn sum_balance_mappings(mut mappings: Vec>>) -> BalanceOf { + let mut output = mappings + .swap_remove(0) + .into_iter() + .map(|user_to_plmc| user_to_plmc.plmc_amount) + .fold(Zero::zero(), |a, b| a + b); + for map in mappings { + output += map.into_iter().map(|user_to_plmc| user_to_plmc.plmc_amount).fold(Zero::zero(), |a, b| a + b); + } + output + } + + pub fn sum_foreign_mappings(mut mappings: Vec>>) -> BalanceOf { + let mut output = mappings + .swap_remove(0) + .into_iter() + .map(|user_to_asset| user_to_asset.asset_amount) + .fold(Zero::zero(), |a, b| a + b); + for map in mappings { + output += map.into_iter().map(|user_to_asset| user_to_asset.asset_amount).fold(Zero::zero(), |a, b| a + b); + } + output + } + + pub fn generate_successful_evaluations( + project_metadata: ProjectMetadataOf, + evaluators: Vec>, + weights: Vec, + ) -> Vec> { + let funding_target = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); + let evaluation_success_threshold = ::EvaluationSuccessThreshold::get(); // if we use just the threshold, then for big usd targets we lose the evaluation due to PLMC conversion errors in `evaluation_end` + let usd_threshold = evaluation_success_threshold * funding_target * 2u32.into(); + + zip(evaluators, weights) + .map(|(evaluator, weight)| { + let ticket_size = Percent::from_percent(weight) * usd_threshold; + (evaluator, ticket_size).into() + }) + .collect() + } + + pub fn generate_bids_from_total_usd( + usd_amount: BalanceOf, + min_price: PriceOf, + weights: Vec, + bidders: Vec>, + multipliers: Vec, + ) -> Vec> { + assert_eq!(weights.len(), bidders.len(), "Should have enough weights for all the bidders"); + + zip(zip(weights, bidders), multipliers) + .map(|((weight, bidder), multiplier)| { + let ticket_size = Percent::from_percent(weight) * usd_amount; + let token_amount = min_price.reciprocal().unwrap().saturating_mul_int(ticket_size); + + BidParams::new(bidder, token_amount, multiplier, AcceptedFundingAsset::USDT) + }) + .collect() + } + + pub fn generate_bids_from_total_ct_percent( + project_metadata: ProjectMetadataOf, + percent_funding: u8, + weights: Vec, + bidders: Vec>, + multipliers: Vec, + ) -> Vec> { + let total_allocation_size = project_metadata.total_allocation_size; + let total_ct_bid = Percent::from_percent(percent_funding) * total_allocation_size; + + assert_eq!(weights.len(), bidders.len(), "Should have enough weights for all the bidders"); + + zip(zip(weights, bidders), multipliers) + .map(|((weight, bidder), multiplier)| { + let token_amount = Percent::from_percent(weight) * total_ct_bid; + BidParams::new(bidder, token_amount, multiplier, AcceptedFundingAsset::USDT) + }) + .collect() + } + + pub fn generate_contributions_from_total_usd( + usd_amount: BalanceOf, + final_price: PriceOf, + weights: Vec, + contributors: Vec>, + multipliers: Vec, + ) -> Vec> { + zip(zip(weights, contributors), multipliers) + .map(|((weight, bidder), multiplier)| { + let ticket_size = Percent::from_percent(weight) * usd_amount; + let token_amount = final_price.reciprocal().unwrap().saturating_mul_int(ticket_size); + + ContributionParams::new(bidder, token_amount, multiplier, AcceptedFundingAsset::USDT) + }) + .collect() + } + + pub fn generate_contributions_from_total_ct_percent( + project_metadata: ProjectMetadataOf, + percent_funding: u8, + weights: Vec, + contributors: Vec>, + multipliers: Vec, + ) -> Vec> { + let total_allocation_size = project_metadata.total_allocation_size; + let total_ct_bought = Percent::from_percent(percent_funding) * total_allocation_size; + + assert_eq!(weights.len(), contributors.len(), "Should have enough weights for all the bidders"); + + zip(zip(weights, contributors), multipliers) + .map(|((weight, contributor), multiplier)| { + let token_amount = Percent::from_percent(weight) * total_ct_bought; + ContributionParams::new(contributor, token_amount, multiplier, AcceptedFundingAsset::USDT) + }) + .collect() + } + + pub fn slash_evaluator_balances(mut balances: Vec>) -> Vec> { + let slash_percentage = ::EvaluatorSlash::get(); + for UserToPLMCBalance { account: _acc, plmc_amount: balance } in balances.iter_mut() { + *balance -= slash_percentage * *balance; + } + balances + } + + pub fn calculate_total_reward_for_evaluation( + evaluation: EvaluationInfoOf, + reward_info: RewardInfoOf, + ) -> BalanceOf { + let early_reward_weight = + Perquintill::from_rational(evaluation.early_usd_amount, reward_info.early_evaluator_total_bonded_usd); + let normal_reward_weight = Perquintill::from_rational( + evaluation.late_usd_amount.saturating_add(evaluation.early_usd_amount), + reward_info.normal_evaluator_total_bonded_usd, + ); + let early_evaluators_rewards = early_reward_weight * reward_info.early_evaluator_reward_pot; + let normal_evaluators_rewards = normal_reward_weight * reward_info.normal_evaluator_reward_pot; + + early_evaluators_rewards.saturating_add(normal_evaluators_rewards) + } +} diff --git a/pallets/funding/src/instantiator/chain_interactions.rs b/pallets/funding/src/instantiator/chain_interactions.rs new file mode 100644 index 000000000..a1fd97a88 --- /dev/null +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -0,0 +1,1075 @@ +use super::*; + +// general chain interactions +impl< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, + > Instantiator +{ + pub fn new(ext: OptionalExternalities) -> Self { + Self { ext, nonce: RefCell::new(0u64), _marker: PhantomData } + } + + pub fn set_ext(&mut self, ext: OptionalExternalities) { + self.ext = ext; + } + + pub fn execute(&mut self, execution: impl FnOnce() -> R) -> R { + #[cfg(feature = "std")] + if let Some(ext) = &self.ext { + return ext.borrow_mut().execute_with(execution); + } + execution() + } + + pub fn get_new_nonce(&self) -> u64 { + let nonce = *self.nonce.borrow_mut(); + self.nonce.replace(nonce + 1); + nonce + } + + pub fn get_free_plmc_balances_for(&mut self, user_keys: Vec>) -> Vec> { + self.execute(|| { + let mut balances: Vec> = Vec::new(); + for account in user_keys { + let plmc_amount = ::NativeCurrency::balance(&account); + balances.push(UserToPLMCBalance { account, plmc_amount }); + } + balances.sort_by_key(|a| a.account.clone()); + balances + }) + } + + pub fn get_reserved_plmc_balances_for( + &mut self, + user_keys: Vec>, + lock_type: ::RuntimeHoldReason, + ) -> Vec> { + self.execute(|| { + let mut balances: Vec> = Vec::new(); + for account in user_keys { + let plmc_amount = ::NativeCurrency::balance_on_hold(&lock_type, &account); + balances.push(UserToPLMCBalance { account, plmc_amount }); + } + balances.sort_by(|a, b| a.account.cmp(&b.account)); + balances + }) + } + + pub fn get_free_foreign_asset_balances_for( + &mut self, + asset_id: AssetIdOf, + user_keys: Vec>, + ) -> Vec> { + self.execute(|| { + let mut balances: Vec> = Vec::new(); + for account in user_keys { + let asset_amount = ::FundingCurrency::balance(asset_id, &account); + balances.push(UserToForeignAssets { account, asset_amount, asset_id }); + } + balances.sort_by(|a, b| a.account.cmp(&b.account)); + balances + }) + } + + pub fn get_ct_asset_balances_for( + &mut self, + project_id: ProjectId, + user_keys: Vec>, + ) -> Vec> { + self.execute(|| { + let mut balances: Vec> = Vec::new(); + for account in user_keys { + let asset_amount = ::ContributionTokenCurrency::balance(project_id, &account); + balances.push(asset_amount); + } + balances + }) + } + + pub fn get_all_free_plmc_balances(&mut self) -> Vec> { + let user_keys = self.execute(|| frame_system::Account::::iter_keys().collect()); + self.get_free_plmc_balances_for(user_keys) + } + + pub fn get_all_reserved_plmc_balances( + &mut self, + reserve_type: ::RuntimeHoldReason, + ) -> Vec> { + let user_keys = self.execute(|| frame_system::Account::::iter_keys().collect()); + self.get_reserved_plmc_balances_for(user_keys, reserve_type) + } + + pub fn get_all_free_foreign_asset_balances(&mut self, asset_id: AssetIdOf) -> Vec> { + let user_keys = self.execute(|| frame_system::Account::::iter_keys().collect()); + self.get_free_foreign_asset_balances_for(asset_id, user_keys) + } + + pub fn get_plmc_total_supply(&mut self) -> BalanceOf { + self.execute(::NativeCurrency::total_issuance) + } + + pub fn do_reserved_plmc_assertions( + &mut self, + correct_funds: Vec>, + reserve_type: ::RuntimeHoldReason, + ) { + for UserToPLMCBalance { account, plmc_amount } in correct_funds { + self.execute(|| { + let reserved = ::NativeCurrency::balance_on_hold(&reserve_type, &account); + assert_eq!(reserved, plmc_amount, "account has unexpected reserved plmc balance"); + }); + } + } + + pub fn mint_plmc_to(&mut self, mapping: Vec>) { + self.execute(|| { + for UserToPLMCBalance { account, plmc_amount } in mapping { + ::NativeCurrency::mint_into(&account, plmc_amount).expect("Minting should work"); + } + }); + } + + pub fn mint_foreign_asset_to(&mut self, mapping: Vec>) { + self.execute(|| { + for UserToForeignAssets { account, asset_amount, asset_id } in mapping { + ::FundingCurrency::mint_into(asset_id, &account, asset_amount) + .expect("Minting should work"); + } + }); + } + + pub fn current_block(&mut self) -> BlockNumberFor { + self.execute(|| frame_system::Pallet::::block_number()) + } + + pub fn advance_time(&mut self, amount: BlockNumberFor) -> Result<(), DispatchError> { + self.execute(|| { + for _block in 0u32..amount.saturated_into() { + let mut current_block = frame_system::Pallet::::block_number(); + + >>::on_finalize(current_block); + as OnFinalize>>::on_finalize(current_block); + + >>::on_idle(current_block, Weight::MAX); + as OnIdle>>::on_idle(current_block, Weight::MAX); + + current_block += One::one(); + frame_system::Pallet::::set_block_number(current_block); + + as OnInitialize>>::on_initialize(current_block); + >>::on_initialize(current_block); + } + Ok(()) + }) + } + + pub fn do_free_plmc_assertions(&mut self, correct_funds: Vec>) { + for UserToPLMCBalance { account, plmc_amount } in correct_funds { + self.execute(|| { + let free = ::NativeCurrency::balance(&account); + assert_eq!(free, plmc_amount, "account has unexpected free plmc balance"); + }); + } + } + + pub fn do_free_foreign_asset_assertions(&mut self, correct_funds: Vec>) { + for UserToForeignAssets { account, asset_amount, asset_id } in correct_funds { + self.execute(|| { + let real_amount = ::FundingCurrency::balance(asset_id, &account); + assert_eq!(asset_amount, real_amount, "Wrong foreign asset balance expected for user {:?}", account); + }); + } + } + + pub fn do_bid_transferred_foreign_asset_assertions( + &mut self, + correct_funds: Vec>, + project_id: ProjectId, + ) { + for UserToForeignAssets { account, asset_amount, .. } in correct_funds { + self.execute(|| { + // total amount of contributions for this user for this project stored in the mapping + let contribution_total: ::Balance = + Bids::::iter_prefix_values((project_id, account.clone())) + .map(|c| c.funding_asset_amount_locked) + .fold(Zero::zero(), |a, b| a + b); + assert_eq!( + contribution_total, asset_amount, + "Wrong funding balance expected for stored auction info on user {:?}", + account + ); + }); + } + } + + // Check if a Contribution storage item exists for the given funding asset transfer + pub fn do_contribution_transferred_foreign_asset_assertions( + &mut self, + correct_funds: Vec>, + project_id: ProjectId, + ) { + for UserToForeignAssets { account, asset_amount, .. } in correct_funds { + self.execute(|| { + Contributions::::iter_prefix_values((project_id, account.clone())) + .find(|c| c.funding_asset_amount == asset_amount) + .expect("Contribution not found in storage"); + }); + } + } +} + +// assertions +impl< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, + > Instantiator +{ + pub fn test_ct_created_for(&mut self, project_id: ProjectId) { + self.execute(|| { + let metadata = ProjectsMetadata::::get(project_id).unwrap(); + assert_eq!( + ::ContributionTokenCurrency::name(project_id), + metadata.token_information.name.to_vec() + ); + let escrow_account = Pallet::::fund_account_id(project_id); + + assert_eq!(::ContributionTokenCurrency::admin(project_id).unwrap(), escrow_account); + }); + } + + pub fn test_ct_not_created_for(&mut self, project_id: ProjectId) { + self.execute(|| { + assert!( + !::ContributionTokenCurrency::asset_exists(project_id), + "Asset shouldn't exist, since funding failed" + ); + }); + } + + pub fn creation_assertions( + &mut self, + project_id: ProjectId, + expected_metadata: ProjectMetadataOf, + creation_start_block: BlockNumberFor, + ) { + let metadata = self.get_project_metadata(project_id); + let details = self.get_project_details(project_id); + let expected_details = ProjectDetailsOf:: { + issuer_account: self.get_issuer(project_id), + issuer_did: generate_did_from_account(self.get_issuer(project_id)), + is_frozen: false, + weighted_average_price: None, + status: ProjectStatus::Application, + phase_transition_points: PhaseTransitionPoints { + application: BlockNumberPair { start: Some(creation_start_block), end: None }, + ..Default::default() + }, + fundraising_target: expected_metadata + .minimum_price + .checked_mul_int(expected_metadata.total_allocation_size) + .unwrap(), + remaining_contribution_tokens: expected_metadata.total_allocation_size, + funding_amount_reached: BalanceOf::::zero(), + evaluation_round_info: EvaluationRoundInfoOf:: { + total_bonded_usd: Zero::zero(), + total_bonded_plmc: Zero::zero(), + evaluators_outcome: EvaluatorsOutcome::Unchanged, + }, + funding_end_block: None, + parachain_id: None, + migration_readiness_check: None, + hrmp_channel_status: HRMPChannelStatus { + project_to_polimec: crate::ChannelStatus::Closed, + polimec_to_project: crate::ChannelStatus::Closed, + }, + }; + assert_eq!(metadata, expected_metadata); + assert_eq!(details, expected_details); + } + + pub fn evaluation_assertions( + &mut self, + project_id: ProjectId, + expected_free_plmc_balances: Vec>, + expected_reserved_plmc_balances: Vec>, + total_plmc_supply: BalanceOf, + ) { + // just in case we forgot to merge accounts: + let expected_free_plmc_balances = + Self::generic_map_operation(vec![expected_free_plmc_balances], MergeOperation::Add); + let expected_reserved_plmc_balances = + Self::generic_map_operation(vec![expected_reserved_plmc_balances], MergeOperation::Add); + + let project_details = self.get_project_details(project_id); + + assert_eq!(project_details.status, ProjectStatus::EvaluationRound); + assert_eq!(self.get_plmc_total_supply(), total_plmc_supply); + self.do_free_plmc_assertions(expected_free_plmc_balances); + self.do_reserved_plmc_assertions(expected_reserved_plmc_balances, HoldReason::Evaluation(project_id).into()); + } + + pub fn finalized_bids_assertions( + &mut self, + project_id: ProjectId, + bid_expectations: Vec>, + expected_ct_sold: BalanceOf, + ) { + let project_metadata = self.get_project_metadata(project_id); + let project_details = self.get_project_details(project_id); + let project_bids = self.execute(|| Bids::::iter_prefix_values((project_id,)).collect::>()); + assert!(project_details.weighted_average_price.is_some(), "Weighted average price should exist"); + + for filter in bid_expectations { + let _found_bid = project_bids.iter().find(|bid| filter.matches_bid(bid)).unwrap(); + } + + // Remaining CTs are updated + assert_eq!( + project_details.remaining_contribution_tokens, + project_metadata.total_allocation_size - expected_ct_sold, + "Remaining CTs are incorrect" + ); + } +} + +// project chain interactions +impl< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, + > Instantiator +{ + pub fn get_issuer(&mut self, project_id: ProjectId) -> AccountIdOf { + self.execute(|| ProjectsDetails::::get(project_id).unwrap().issuer_account) + } + + pub fn get_project_metadata(&mut self, project_id: ProjectId) -> ProjectMetadataOf { + self.execute(|| ProjectsMetadata::::get(project_id).expect("Project metadata exists")) + } + + pub fn get_project_details(&mut self, project_id: ProjectId) -> ProjectDetailsOf { + self.execute(|| ProjectsDetails::::get(project_id).expect("Project details exists")) + } + + pub fn get_update_block(&mut self, project_id: ProjectId, update_type: &UpdateType) -> Option> { + self.execute(|| { + ProjectsToUpdate::::iter().find_map(|(block, update_tup)| { + if project_id == update_tup.0 && update_type == &update_tup.1 { + Some(block) + } else { + None + } + }) + }) + } + + pub fn create_new_project(&mut self, project_metadata: ProjectMetadataOf, issuer: AccountIdOf) -> ProjectId { + let now = self.current_block(); + // one ED for the issuer, one ED for the escrow account + self.mint_plmc_to(vec![UserToPLMCBalance::new(issuer.clone(), Self::get_ed() * 2u64.into())]); + + self.execute(|| { + crate::Pallet::::do_create_project( + &issuer, + project_metadata.clone(), + generate_did_from_account(issuer.clone()), + ) + .unwrap(); + let last_project_metadata = ProjectsMetadata::::iter().last().unwrap(); + log::trace!("Last project metadata: {:?}", last_project_metadata); + }); + + let created_project_id = self.execute(|| NextProjectId::::get().saturating_sub(One::one())); + self.creation_assertions(created_project_id, project_metadata, now); + created_project_id + } + + pub fn start_evaluation(&mut self, project_id: ProjectId, caller: AccountIdOf) -> Result<(), DispatchError> { + assert_eq!(self.get_project_details(project_id).status, ProjectStatus::Application); + self.execute(|| crate::Pallet::::do_start_evaluation(caller, project_id).unwrap()); + assert_eq!(self.get_project_details(project_id).status, ProjectStatus::EvaluationRound); + + Ok(()) + } + + pub fn create_evaluating_project( + &mut self, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + ) -> ProjectId { + let project_id = self.create_new_project(project_metadata, issuer.clone()); + self.start_evaluation(project_id, issuer).unwrap(); + project_id + } + + pub fn evaluate_for_users( + &mut self, + project_id: ProjectId, + bonds: Vec>, + ) -> DispatchResultWithPostInfo { + for UserToUSDBalance { account, usd_amount } in bonds { + self.execute(|| { + crate::Pallet::::do_evaluate( + &account.clone(), + project_id, + usd_amount, + generate_did_from_account(account), + InvestorType::Professional, + ) + })?; + } + Ok(().into()) + } + + pub fn start_auction(&mut self, project_id: ProjectId, caller: AccountIdOf) -> Result<(), DispatchError> { + let project_details = self.get_project_details(project_id); + + if project_details.status == ProjectStatus::EvaluationRound { + let evaluation_end = project_details.phase_transition_points.evaluation.end().unwrap(); + let auction_start = evaluation_end.saturating_add(2u32.into()); + let blocks_to_start = auction_start.saturating_sub(self.current_block()); + self.advance_time(blocks_to_start).unwrap(); + }; + + assert_eq!(self.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); + + self.execute(|| crate::Pallet::::do_auction_opening(caller, project_id).unwrap()); + + assert_eq!(self.get_project_details(project_id).status, ProjectStatus::AuctionOpening); + + Ok(()) + } + + pub fn create_auctioning_project( + &mut self, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + ) -> ProjectId { + let project_id = self.create_evaluating_project(project_metadata, issuer.clone()); + + let evaluators = evaluations.accounts(); + let prev_supply = self.get_plmc_total_supply(); + let prev_plmc_balances = self.get_free_plmc_balances_for(evaluators.clone()); + + let plmc_eval_deposits: Vec> = Self::calculate_evaluation_plmc_spent(evaluations.clone()); + let plmc_existential_deposits: Vec> = evaluators.existential_deposits(); + + let expected_remaining_plmc: Vec> = Self::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + self.mint_plmc_to(plmc_eval_deposits.clone()); + self.mint_plmc_to(plmc_existential_deposits.clone()); + + self.evaluate_for_users(project_id, evaluations).unwrap(); + + let expected_evaluator_balances = + Self::sum_balance_mappings(vec![plmc_eval_deposits.clone(), plmc_existential_deposits.clone()]); + + let expected_total_supply = prev_supply + expected_evaluator_balances; + + self.evaluation_assertions(project_id, expected_remaining_plmc, plmc_eval_deposits, expected_total_supply); + + self.start_auction(project_id, issuer).unwrap(); + project_id + } + + pub fn bid_for_users(&mut self, project_id: ProjectId, bids: Vec>) -> DispatchResultWithPostInfo { + for bid in bids { + self.execute(|| { + let did = generate_did_from_account(bid.bidder.clone()); + crate::Pallet::::do_bid( + &bid.bidder, + project_id, + bid.amount, + bid.multiplier, + bid.asset, + did, + InvestorType::Institutional, + ) + })?; + } + Ok(().into()) + } + + pub fn start_community_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { + let opening_end = self + .get_project_details(project_id) + .phase_transition_points + .auction_opening + .end() + .expect("Auction Opening end point should exist"); + + self.execute(|| frame_system::Pallet::::set_block_number(opening_end)); + // run on_initialize + self.advance_time(1u32.into()).unwrap(); + + let closing_end = self + .get_project_details(project_id) + .phase_transition_points + .auction_closing + .end() + .expect("closing end point should exist"); + + self.execute(|| frame_system::Pallet::::set_block_number(closing_end)); + // run on_initialize + self.advance_time(1u32.into()).unwrap(); + + ensure!( + self.get_project_details(project_id).status == ProjectStatus::CommunityRound, + DispatchError::from("Auction failed") + ); + + Ok(()) + } + + pub fn create_community_contributing_project( + &mut self, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, + ) -> ProjectId { + if bids.is_empty() { + panic!("Cannot start community funding without bids") + } + + let project_id = self.create_auctioning_project(project_metadata.clone(), issuer, evaluations.clone()); + let bidders = bids.accounts(); + let asset_id = bids[0].asset.to_assethub_id(); + let prev_plmc_balances = self.get_free_plmc_balances_for(bidders.clone()); + let prev_funding_asset_balances = self.get_free_foreign_asset_balances_for(asset_id, bidders.clone()); + let plmc_evaluation_deposits: Vec> = Self::calculate_evaluation_plmc_spent(evaluations); + let plmc_bid_deposits: Vec> = + Self::calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata.clone(), + None, + ); + let participation_usable_evaluation_deposits = plmc_evaluation_deposits + .into_iter() + .map(|mut x| { + x.plmc_amount = x.plmc_amount.saturating_sub(::EvaluatorSlash::get() * x.plmc_amount); + x + }) + .collect::>>(); + let necessary_plmc_mint = Self::generic_map_operation( + vec![plmc_bid_deposits.clone(), participation_usable_evaluation_deposits], + MergeOperation::Subtract, + ); + let total_plmc_participation_locked = plmc_bid_deposits; + let plmc_existential_deposits: Vec> = bidders.existential_deposits(); + let funding_asset_deposits = Self::calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata.clone(), + None, + ); + + let bidder_balances = + Self::sum_balance_mappings(vec![necessary_plmc_mint.clone(), plmc_existential_deposits.clone()]); + + let expected_free_plmc_balances = Self::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + let prev_supply = self.get_plmc_total_supply(); + let post_supply = prev_supply + bidder_balances; + + self.mint_plmc_to(necessary_plmc_mint.clone()); + self.mint_plmc_to(plmc_existential_deposits.clone()); + self.mint_foreign_asset_to(funding_asset_deposits.clone()); + + self.bid_for_users(project_id, bids.clone()).unwrap(); + + self.do_reserved_plmc_assertions( + total_plmc_participation_locked.merge_accounts(MergeOperation::Add), + HoldReason::Participation(project_id).into(), + ); + self.do_bid_transferred_foreign_asset_assertions( + funding_asset_deposits.merge_accounts(MergeOperation::Add), + project_id, + ); + self.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); + self.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); + assert_eq!(self.get_plmc_total_supply(), post_supply); + + self.start_community_funding(project_id).unwrap(); + + project_id + } + + pub fn contribute_for_users( + &mut self, + project_id: ProjectId, + contributions: Vec>, + ) -> DispatchResultWithPostInfo { + match self.get_project_details(project_id).status { + ProjectStatus::CommunityRound => + for cont in contributions { + let did = generate_did_from_account(cont.contributor.clone()); + let investor_type = InvestorType::Retail; + self.execute(|| { + crate::Pallet::::do_community_contribute( + &cont.contributor, + project_id, + cont.amount, + cont.multiplier, + cont.asset, + did, + investor_type, + ) + })?; + }, + ProjectStatus::RemainderRound => + for cont in contributions { + let did = generate_did_from_account(cont.contributor.clone()); + let investor_type = InvestorType::Professional; + self.execute(|| { + crate::Pallet::::do_remaining_contribute( + &cont.contributor, + project_id, + cont.amount, + cont.multiplier, + cont.asset, + did, + investor_type, + ) + })?; + }, + _ => panic!("Project should be in Community or Remainder status"), + } + + Ok(().into()) + } + + pub fn start_remainder_or_end_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { + let details = self.get_project_details(project_id); + assert_eq!(details.status, ProjectStatus::CommunityRound); + let remaining_tokens = details.remaining_contribution_tokens; + let update_type = + if remaining_tokens > Zero::zero() { UpdateType::RemainderFundingStart } else { UpdateType::FundingEnd }; + if let Some(transition_block) = self.get_update_block(project_id, &update_type) { + self.execute(|| frame_system::Pallet::::set_block_number(transition_block - One::one())); + self.advance_time(1u32.into()).unwrap(); + match self.get_project_details(project_id).status { + ProjectStatus::RemainderRound | ProjectStatus::FundingSuccessful => Ok(()), + _ => panic!("Bad state"), + } + } else { + panic!("Bad state") + } + } + + pub fn finish_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { + if let Some(update_block) = self.get_update_block(project_id, &UpdateType::RemainderFundingStart) { + self.execute(|| frame_system::Pallet::::set_block_number(update_block - One::one())); + self.advance_time(1u32.into()).unwrap(); + } + let update_block = + self.get_update_block(project_id, &UpdateType::FundingEnd).expect("Funding end block should exist"); + self.execute(|| frame_system::Pallet::::set_block_number(update_block - One::one())); + self.advance_time(1u32.into()).unwrap(); + let project_details = self.get_project_details(project_id); + assert!( + matches!( + project_details.status, + ProjectStatus::FundingSuccessful | + ProjectStatus::FundingFailed | + ProjectStatus::AwaitingProjectDecision + ), + "Project should be in Finished status" + ); + Ok(()) + } + + pub fn settle_project(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { + let details = self.get_project_details(project_id); + self.execute(|| match details.status { + ProjectStatus::FundingSuccessful => Self::settle_successful_project(project_id), + ProjectStatus::FundingFailed => Self::settle_failed_project(project_id), + _ => panic!("Project should be in FundingSuccessful or FundingFailed status"), + }) + } + + fn settle_successful_project(project_id: ProjectId) -> Result<(), DispatchError> { + Evaluations::::iter_prefix((project_id,)) + .try_for_each(|(_, evaluation)| Pallet::::do_settle_successful_evaluation(evaluation, project_id))?; + + Bids::::iter_prefix((project_id,)) + .try_for_each(|(_, bid)| Pallet::::do_settle_successful_bid(bid, project_id))?; + + Contributions::::iter_prefix((project_id,)) + .try_for_each(|(_, contribution)| Pallet::::do_settle_successful_contribution(contribution, project_id)) + } + + fn settle_failed_project(project_id: ProjectId) -> Result<(), DispatchError> { + Evaluations::::iter_prefix((project_id,)) + .try_for_each(|(_, evaluation)| Pallet::::do_settle_failed_evaluation(evaluation, project_id))?; + + Bids::::iter_prefix((project_id,)) + .try_for_each(|(_, bid)| Pallet::::do_settle_failed_bid(bid, project_id))?; + + Contributions::::iter_prefix((project_id,)) + .try_for_each(|(_, contribution)| Pallet::::do_settle_failed_contribution(contribution, project_id))?; + + Ok(()) + } + + pub fn get_evaluations(&mut self, project_id: ProjectId) -> Vec> { + self.execute(|| Evaluations::::iter_prefix_values((project_id,)).collect()) + } + + pub fn get_bids(&mut self, project_id: ProjectId) -> Vec> { + self.execute(|| Bids::::iter_prefix_values((project_id,)).collect()) + } + + pub fn get_contributions(&mut self, project_id: ProjectId) -> Vec> { + self.execute(|| Contributions::::iter_prefix_values((project_id,)).collect()) + } + + // Used to check if all evaluations are settled correctly. We cannot check amount of + // contributions minted for the user, as they could have received more tokens from other participations. + pub fn assert_evaluations_migrations_created( + &mut self, + project_id: ProjectId, + evaluations: Vec>, + percentage: u64, + ) { + let details = self.get_project_details(project_id); + assert!(matches!(details.status, ProjectStatus::FundingSuccessful | ProjectStatus::FundingFailed)); + + self.execute(|| { + for evaluation in evaluations { + let reward_info = + ProjectsDetails::::get(project_id).unwrap().evaluation_round_info.evaluators_outcome; + let account = evaluation.evaluator.clone(); + assert_eq!(Evaluations::::iter_prefix_values((&project_id, &account)).count(), 0); + + let (amount, should_exist) = match percentage { + 0..=75 => { + assert!(matches!(reward_info, EvaluatorsOutcome::Slashed)); + (0u64.into(), false) + }, + 76..=89 => { + assert!(matches!(reward_info, EvaluatorsOutcome::Unchanged)); + (0u64.into(), false) + }, + 90..=100 => { + let reward = match reward_info { + EvaluatorsOutcome::Rewarded(info) => + Pallet::::calculate_evaluator_reward(&evaluation, &info), + _ => panic!("Evaluators should be rewarded"), + }; + (reward, true) + }, + _ => panic!("Percentage should be between 0 and 100"), + }; + Self::assert_migration( + project_id, + account, + amount, + evaluation.id, + ParticipationType::Evaluation, + should_exist, + ); + } + }); + } + + // Testing if a list of bids are settled correctly. + pub fn assert_bids_migrations_created( + &mut self, + project_id: ProjectId, + bids: Vec>, + is_successful: bool, + ) { + self.execute(|| { + for bid in bids { + let account = bid.bidder.clone(); + assert_eq!(Bids::::iter_prefix_values((&project_id, &account)).count(), 0); + let amount: BalanceOf = if is_successful { bid.final_ct_amount } else { 0u64.into() }; + Self::assert_migration(project_id, account, amount, bid.id, ParticipationType::Bid, is_successful); + } + }); + } + + // Testing if a list of contributions are settled correctly. + pub fn assert_contributions_migrations_created( + &mut self, + project_id: ProjectId, + contributions: Vec>, + is_successful: bool, + ) { + self.execute(|| { + for contribution in contributions { + let account = contribution.contributor.clone(); + assert_eq!(Bids::::iter_prefix_values((&project_id, &account)).count(), 0); + let amount: BalanceOf = if is_successful { contribution.ct_amount } else { 0u64.into() }; + Self::assert_migration( + project_id, + account, + amount, + contribution.id, + ParticipationType::Contribution, + is_successful, + ); + } + }); + } + + fn assert_migration( + project_id: ProjectId, + account: AccountIdOf, + amount: BalanceOf, + id: u32, + participation_type: ParticipationType, + should_exist: bool, + ) { + let correct = match (should_exist, UserMigrations::::get(project_id, account.clone())) { + // User has migrations, so we need to check if any matches our criteria + (_, Some((_, migrations))) => { + let maybe_migration = migrations.into_iter().find(|migration| { + let user = T::AccountId32Conversion::convert(account.clone()); + matches!(migration.origin, MigrationOrigin { user: m_user, id: m_id, participation_type: m_participation_type } if m_user == user && m_id == id && m_participation_type == participation_type) + }); + match maybe_migration { + // Migration exists so we check if the amount is correct and if it should exist + Some(migration) => migration.info.contribution_token_amount == amount.into() && should_exist, + // Migration doesn't exist so we check if it should not exist + None => !should_exist, + } + }, + // User does not have any migrations, so the migration should not exist + (false, None) => true, + (true, None) => false, + }; + assert!(correct); + } + + pub fn create_remainder_contributing_project( + &mut self, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, + contributions: Vec>, + ) -> ProjectId { + let project_id = self.create_community_contributing_project( + project_metadata.clone(), + issuer, + evaluations.clone(), + bids.clone(), + ); + + if contributions.is_empty() { + self.start_remainder_or_end_funding(project_id).unwrap(); + return project_id; + } + + let ct_price = self.get_project_details(project_id).weighted_average_price.unwrap(); + + let contributors = contributions.accounts(); + + let asset_id = contributions[0].asset.to_assethub_id(); + + let prev_plmc_balances = self.get_free_plmc_balances_for(contributors.clone()); + let prev_funding_asset_balances = self.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); + + let plmc_evaluation_deposits = Self::calculate_evaluation_plmc_spent(evaluations.clone()); + let plmc_bid_deposits = Self::calculate_auction_plmc_spent_post_wap(&bids, project_metadata.clone(), ct_price); + let plmc_contribution_deposits = Self::calculate_contributed_plmc_spent(contributions.clone(), ct_price); + + let reducible_evaluator_balances = Self::slash_evaluator_balances(plmc_evaluation_deposits.clone()); + let necessary_plmc_mint = Self::generic_map_operation( + vec![plmc_contribution_deposits.clone(), reducible_evaluator_balances], + MergeOperation::Subtract, + ); + let total_plmc_participation_locked = + Self::generic_map_operation(vec![plmc_bid_deposits, plmc_contribution_deposits], MergeOperation::Add); + let plmc_existential_deposits = contributors.existential_deposits(); + + let funding_asset_deposits = Self::calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); + let contributor_balances = + Self::sum_balance_mappings(vec![necessary_plmc_mint.clone(), plmc_existential_deposits.clone()]); + + let expected_free_plmc_balances = Self::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + let prev_supply = self.get_plmc_total_supply(); + let post_supply = prev_supply + contributor_balances; + + self.mint_plmc_to(necessary_plmc_mint.clone()); + self.mint_plmc_to(plmc_existential_deposits.clone()); + self.mint_foreign_asset_to(funding_asset_deposits.clone()); + + self.contribute_for_users(project_id, contributions).expect("Contributing should work"); + + self.do_reserved_plmc_assertions( + total_plmc_participation_locked.merge_accounts(MergeOperation::Add), + HoldReason::Participation(project_id).into(), + ); + + self.do_contribution_transferred_foreign_asset_assertions(funding_asset_deposits, project_id); + + self.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); + self.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); + assert_eq!(self.get_plmc_total_supply(), post_supply); + + self.start_remainder_or_end_funding(project_id).unwrap(); + + project_id + } + + pub fn create_finished_project( + &mut self, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, + community_contributions: Vec>, + remainder_contributions: Vec>, + ) -> ProjectId { + let project_id = self.create_remainder_contributing_project( + project_metadata.clone(), + issuer, + evaluations.clone(), + bids.clone(), + community_contributions.clone(), + ); + + match self.get_project_details(project_id).status { + ProjectStatus::FundingSuccessful => return project_id, + ProjectStatus::RemainderRound if remainder_contributions.is_empty() => { + self.finish_funding(project_id).unwrap(); + return project_id; + }, + _ => {}, + }; + + let ct_price = self.get_project_details(project_id).weighted_average_price.unwrap(); + let contributors = remainder_contributions.accounts(); + let asset_id = remainder_contributions[0].asset.to_assethub_id(); + let prev_plmc_balances = self.get_free_plmc_balances_for(contributors.clone()); + let prev_funding_asset_balances = self.get_free_foreign_asset_balances_for(asset_id, contributors.clone()); + + let plmc_evaluation_deposits = Self::calculate_evaluation_plmc_spent(evaluations); + let plmc_bid_deposits = Self::calculate_auction_plmc_spent_post_wap(&bids, project_metadata.clone(), ct_price); + let plmc_community_contribution_deposits = + Self::calculate_contributed_plmc_spent(community_contributions.clone(), ct_price); + let plmc_remainder_contribution_deposits = + Self::calculate_contributed_plmc_spent(remainder_contributions.clone(), ct_price); + + let necessary_plmc_mint = Self::generic_map_operation( + vec![plmc_remainder_contribution_deposits.clone(), plmc_evaluation_deposits], + MergeOperation::Subtract, + ); + let total_plmc_participation_locked = Self::generic_map_operation( + vec![plmc_bid_deposits, plmc_community_contribution_deposits, plmc_remainder_contribution_deposits], + MergeOperation::Add, + ); + let plmc_existential_deposits = contributors.existential_deposits(); + let funding_asset_deposits = + Self::calculate_contributed_funding_asset_spent(remainder_contributions.clone(), ct_price); + + let contributor_balances = + Self::sum_balance_mappings(vec![necessary_plmc_mint.clone(), plmc_existential_deposits.clone()]); + + let expected_free_plmc_balances = Self::generic_map_operation( + vec![prev_plmc_balances, plmc_existential_deposits.clone()], + MergeOperation::Add, + ); + + let prev_supply = self.get_plmc_total_supply(); + let post_supply = prev_supply + contributor_balances; + + self.mint_plmc_to(necessary_plmc_mint.clone()); + self.mint_plmc_to(plmc_existential_deposits.clone()); + self.mint_foreign_asset_to(funding_asset_deposits.clone()); + + self.contribute_for_users(project_id, remainder_contributions.clone()) + .expect("Remainder Contributing should work"); + + self.do_reserved_plmc_assertions( + total_plmc_participation_locked.merge_accounts(MergeOperation::Add), + HoldReason::Participation(project_id).into(), + ); + self.do_contribution_transferred_foreign_asset_assertions( + funding_asset_deposits.merge_accounts(MergeOperation::Add), + project_id, + ); + self.do_free_plmc_assertions(expected_free_plmc_balances.merge_accounts(MergeOperation::Add)); + self.do_free_foreign_asset_assertions(prev_funding_asset_balances.merge_accounts(MergeOperation::Add)); + assert_eq!(self.get_plmc_total_supply(), post_supply); + + self.finish_funding(project_id).unwrap(); + + if self.get_project_details(project_id).status == ProjectStatus::FundingSuccessful { + // Check that remaining CTs are updated + let project_details = self.get_project_details(project_id); + // if our bids were creating an oversubscription, then just take the total allocation size + let auction_bought_tokens = bids + .iter() + .map(|bid| bid.amount) + .fold(Zero::zero(), |acc, item| item + acc) + .min(project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size); + let community_bought_tokens = + community_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); + let remainder_bought_tokens = + remainder_contributions.iter().map(|cont| cont.amount).fold(Zero::zero(), |acc, item| item + acc); + + assert_eq!( + project_details.remaining_contribution_tokens, + project_metadata.total_allocation_size - + auction_bought_tokens - + community_bought_tokens - + remainder_bought_tokens, + "Remaining CTs are incorrect" + ); + } + + project_id + } + + pub fn create_project_at( + &mut self, + status: ProjectStatus, + project_metadata: ProjectMetadataOf, + issuer: AccountIdOf, + evaluations: Vec>, + bids: Vec>, + community_contributions: Vec>, + remainder_contributions: Vec>, + ) -> ProjectId { + match status { + ProjectStatus::FundingSuccessful => self.create_finished_project( + project_metadata, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ), + ProjectStatus::RemainderRound => self.create_remainder_contributing_project( + project_metadata, + issuer, + evaluations, + bids, + community_contributions, + ), + ProjectStatus::CommunityRound => + self.create_community_contributing_project(project_metadata, issuer, evaluations, bids), + ProjectStatus::AuctionOpening => self.create_auctioning_project(project_metadata, issuer, evaluations), + ProjectStatus::EvaluationRound => self.create_evaluating_project(project_metadata, issuer), + ProjectStatus::Application => self.create_new_project(project_metadata, issuer), + _ => panic!("unsupported project creation in that status"), + } + } +} diff --git a/pallets/funding/src/instantiator/macros.rs b/pallets/funding/src/instantiator/macros.rs new file mode 100644 index 000000000..60838906f --- /dev/null +++ b/pallets/funding/src/instantiator/macros.rs @@ -0,0 +1,115 @@ +use super::*; + +#[macro_export] +/// Example: +/// ``` +/// use pallet_funding::assert_close_enough; +/// use sp_arithmetic::Perquintill; +/// +/// let real = 98u64; +/// let desired = 100u64; +/// assert_close_enough!(real, desired, Perquintill::from_float(0.98)); +/// // This would fail +/// // assert_close_enough!(real, desired, Perquintill::from_float(0.99)); +/// ``` +macro_rules! assert_close_enough { + // Match when a message is provided + ($real:expr, $desired:expr, $min_percentage:expr, $msg:expr) => { + let actual_percentage; + if $real <= $desired { + actual_percentage = Perquintill::from_rational($real, $desired); + } else { + actual_percentage = Perquintill::from_rational($desired, $real); + } + assert!(actual_percentage >= $min_percentage, $msg); + }; + // Match when no message is provided + ($real:expr, $desired:expr, $min_percentage:expr) => { + let actual_percentage; + if $real <= $desired { + actual_percentage = Perquintill::from_rational($real, $desired); + } else { + actual_percentage = Perquintill::from_rational($desired, $real); + } + assert!( + actual_percentage >= $min_percentage, + "Actual percentage too low for the set minimum: {:?} < {:?} for {:?} and {:?}", + actual_percentage, + $min_percentage, + $real, + $desired + ); + }; +} + +#[macro_export] +macro_rules! call_and_is_ok { + ($inst: expr, $( $call: expr ),* ) => { + $inst.execute(|| { + $( + let result = $call; + assert!(result.is_ok(), "Call failed: {:?}", result); + )* + }) + }; + } +#[macro_export] +macro_rules! find_event { + ($runtime:ty, $pattern:pat, $($field_name:ident == $field_value:expr),+) => { + { + let events = frame_system::Pallet::<$runtime>::events(); + events.iter().find_map(|event_record| { + let runtime_event = event_record.event.clone(); + let runtime_event = <<$runtime as crate::Config>::RuntimeEvent>::from(runtime_event); + if let Ok(funding_event) = TryInto::>::try_into(runtime_event) { + if let $pattern = funding_event { + let mut is_match = true; + $( + is_match &= $field_name == $field_value; + )+ + if is_match { + return Some(funding_event.clone()); + } + } + None + } else { + None + } + }) + } + }; +} + +#[macro_export] +macro_rules! extract_from_event { + ($env: expr, $pattern:pat, $field:ident) => { + $env.execute(|| { + let events = System::events(); + + events.iter().find_map(|event_record| { + if let frame_system::EventRecord { event: RuntimeEvent::PolimecFunding($pattern), .. } = event_record { + Some($field.clone()) + } else { + None + } + }) + }) + }; +} + +#[macro_export] +macro_rules! define_names { + ($($name:ident: $id:expr, $label:expr);* $(;)?) => { + $( + pub const $name: AccountId = $id; + )* + + pub fn names() -> std::collections::HashMap { + let mut names = std::collections::HashMap::new(); + $( + names.insert($name, $label); + )* + names + } + }; + } diff --git a/pallets/funding/src/instantiator/mod.rs b/pallets/funding/src/instantiator/mod.rs new file mode 100644 index 000000000..df7c8cd9b --- /dev/null +++ b/pallets/funding/src/instantiator/mod.rs @@ -0,0 +1,64 @@ +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// The Polimec Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Polimec Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{ + traits::{BondingRequirementCalculation, ProvideAssetPrice}, + *, +}; +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Inspect as FungibleInspect, InspectHold as FungibleInspectHold, Mutate as FungibleMutate}, + fungibles::{ + metadata::Inspect as MetadataInspect, roles::Inspect as RolesInspect, Inspect as FungiblesInspect, + Mutate as FungiblesMutate, + }, + AccountTouch, Get, OnFinalize, OnIdle, OnInitialize, + }, + weights::Weight, + Parameter, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use itertools::Itertools; +use parity_scale_codec::Decode; +use polimec_common::{credentials::InvestorType, migration_types::MigrationOrigin}; +#[cfg(any(test, feature = "std", feature = "runtime-benchmarks"))] +use polimec_common_test_utils::generate_did_from_account; +use sp_arithmetic::{ + traits::{SaturatedConversion, Saturating, Zero}, + FixedPointNumber, Percent, Perquintill, +}; +use sp_runtime::{ + traits::{Convert, Member, One}, + DispatchError, +}; +use sp_std::{ + cell::RefCell, + collections::{btree_map::*, btree_set::*}, + iter::zip, + marker::PhantomData, +}; + +pub mod macros; +pub use macros::*; +pub mod types; +pub use types::*; +pub mod traits; +pub use traits::*; +#[cfg(feature = "std")] +pub mod async_features; +pub mod calculations; +pub mod chain_interactions; diff --git a/pallets/funding/src/instantiator/traits.rs b/pallets/funding/src/instantiator/traits.rs new file mode 100644 index 000000000..5eee8ccaa --- /dev/null +++ b/pallets/funding/src/instantiator/traits.rs @@ -0,0 +1,26 @@ +use super::*; + +pub trait Deposits { + fn existential_deposits(&self) -> Vec>; +} +pub trait Accounts { + type Account; + + fn accounts(&self) -> Vec; +} + +pub enum MergeOperation { + Add, + Subtract, +} +pub trait AccountMerge: Accounts + Sized { + /// The inner type of the Vec implementing this Trait. + type Inner; + /// Merge accounts in the list based on the operation. + fn merge_accounts(&self, ops: MergeOperation) -> Self; + /// Subtract amount of the matching accounts in the other list from the current list. + /// If the account is not present in the current list, it is ignored. + fn subtract_accounts(&self, other_list: Self) -> Self; + + fn sum_accounts(&self, other_list: Self) -> Self; +} diff --git a/pallets/funding/src/instantiator/types.rs b/pallets/funding/src/instantiator/types.rs new file mode 100644 index 000000000..25fe56688 --- /dev/null +++ b/pallets/funding/src/instantiator/types.rs @@ -0,0 +1,452 @@ +use super::*; + +pub type RuntimeOriginOf = ::RuntimeOrigin; +pub struct BoxToFunction(pub Box); +impl Default for BoxToFunction { + fn default() -> Self { + BoxToFunction(Box::new(|| ())) + } +} + +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "std", + serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) +)] +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub struct TestProjectParams { + pub expected_state: ProjectStatus, + pub metadata: ProjectMetadataOf, + pub issuer: AccountIdOf, + pub evaluations: Vec>, + pub bids: Vec>, + pub community_contributions: Vec>, + pub remainder_contributions: Vec>, +} + +#[cfg(feature = "std")] +pub type OptionalExternalities = Option>; + +#[cfg(not(feature = "std"))] +pub type OptionalExternalities = Option<()>; + +pub struct Instantiator< + T: Config + pallet_balances::Config>, + AllPalletsWithoutSystem: OnFinalize> + OnIdle> + OnInitialize>, + RuntimeEvent: From> + TryInto> + Parameter + Member + IsType<::RuntimeEvent>, +> { + pub ext: OptionalExternalities, + pub nonce: RefCell, + pub _marker: PhantomData<(T, AllPalletsWithoutSystem, RuntimeEvent)>, +} + +impl Deposits for Vec> { + fn existential_deposits(&self) -> Vec> { + self.iter() + .map(|x| UserToPLMCBalance::new(x.clone(), ::ExistentialDeposit::get())) + .collect::>() + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct UserToPLMCBalance { + pub account: AccountIdOf, + pub plmc_amount: BalanceOf, +} +impl UserToPLMCBalance { + pub fn new(account: AccountIdOf, plmc_amount: BalanceOf) -> Self { + Self { account, plmc_amount } + } +} +impl Accounts for Vec> { + type Account = AccountIdOf; + + fn accounts(&self) -> Vec { + let mut btree = BTreeSet::new(); + for UserToPLMCBalance { account, plmc_amount: _ } in self.iter() { + btree.insert(account.clone()); + } + btree.into_iter().collect_vec() + } +} +impl From<(AccountIdOf, BalanceOf)> for UserToPLMCBalance { + fn from((account, plmc_amount): (AccountIdOf, BalanceOf)) -> Self { + UserToPLMCBalance::::new(account, plmc_amount) + } +} +impl AccountMerge for Vec> { + type Inner = UserToPLMCBalance; + + fn merge_accounts(&self, ops: MergeOperation) -> Self { + let mut btree = BTreeMap::new(); + for UserToPLMCBalance { account, plmc_amount } in self.iter() { + btree + .entry(account.clone()) + .and_modify(|e: &mut BalanceOf| { + *e = match ops { + MergeOperation::Add => e.saturating_add(*plmc_amount), + MergeOperation::Subtract => e.saturating_sub(*plmc_amount), + } + }) + .or_insert(*plmc_amount); + } + btree.into_iter().map(|(account, plmc_amount)| UserToPLMCBalance::new(account, plmc_amount)).collect() + } + + fn subtract_accounts(&self, other_list: Self) -> Self { + let current_accounts = self.accounts(); + let filtered_list = other_list.into_iter().filter(|x| current_accounts.contains(&x.account)).collect_vec(); + let mut new_list = self.clone(); + new_list.extend(filtered_list); + new_list.merge_accounts(MergeOperation::Subtract) + } + + fn sum_accounts(&self, mut other_list: Self) -> Self { + let mut output = self.clone(); + output.append(&mut other_list); + output.merge_accounts(MergeOperation::Add) + } +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "std", + serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) +)] +pub struct UserToUSDBalance { + pub account: AccountIdOf, + pub usd_amount: BalanceOf, +} +impl UserToUSDBalance { + pub fn new(account: AccountIdOf, usd_amount: BalanceOf) -> Self { + Self { account, usd_amount } + } +} +impl From<(AccountIdOf, BalanceOf)> for UserToUSDBalance { + fn from((account, usd_amount): (AccountIdOf, BalanceOf)) -> Self { + UserToUSDBalance::::new(account, usd_amount) + } +} +impl Accounts for Vec> { + type Account = AccountIdOf; + + fn accounts(&self) -> Vec { + let mut btree = BTreeSet::new(); + for UserToUSDBalance { account, usd_amount: _ } in self { + btree.insert(account.clone()); + } + btree.into_iter().collect_vec() + } +} +impl AccountMerge for Vec> { + type Inner = UserToUSDBalance; + + fn merge_accounts(&self, ops: MergeOperation) -> Self { + let mut btree = BTreeMap::new(); + for UserToUSDBalance { account, usd_amount } in self.iter() { + btree + .entry(account.clone()) + .and_modify(|e: &mut BalanceOf| { + *e = match ops { + MergeOperation::Add => e.saturating_add(*usd_amount), + MergeOperation::Subtract => e.saturating_sub(*usd_amount), + } + }) + .or_insert(*usd_amount); + } + btree.into_iter().map(|(account, usd_amount)| UserToUSDBalance::new(account, usd_amount)).collect() + } + + fn subtract_accounts(&self, other_list: Self) -> Self { + let current_accounts = self.accounts(); + let filtered_list = other_list.into_iter().filter(|x| current_accounts.contains(&x.account)).collect_vec(); + let mut new_list = self.clone(); + new_list.extend(filtered_list); + new_list.merge_accounts(MergeOperation::Subtract) + } + + fn sum_accounts(&self, mut other_list: Self) -> Self { + let mut output = self.clone(); + output.append(&mut other_list); + output.merge_accounts(MergeOperation::Add) + } +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct UserToForeignAssets { + pub account: AccountIdOf, + pub asset_amount: BalanceOf, + pub asset_id: AssetIdOf, +} +impl UserToForeignAssets { + pub fn new(account: AccountIdOf, asset_amount: BalanceOf, asset_id: AssetIdOf) -> Self { + Self { account, asset_amount, asset_id } + } +} +impl From<(AccountIdOf, BalanceOf, AssetIdOf)> for UserToForeignAssets { + fn from((account, asset_amount, asset_id): (AccountIdOf, BalanceOf, AssetIdOf)) -> Self { + UserToForeignAssets::::new(account, asset_amount, asset_id) + } +} +impl From<(AccountIdOf, BalanceOf)> for UserToForeignAssets { + fn from((account, asset_amount): (AccountIdOf, BalanceOf)) -> Self { + UserToForeignAssets::::new(account, asset_amount, AcceptedFundingAsset::USDT.to_assethub_id()) + } +} +impl Accounts for Vec> { + type Account = AccountIdOf; + + fn accounts(&self) -> Vec { + let mut btree = BTreeSet::new(); + for UserToForeignAssets { account, .. } in self.iter() { + btree.insert(account.clone()); + } + btree.into_iter().collect_vec() + } +} +impl AccountMerge for Vec> { + type Inner = UserToForeignAssets; + + fn merge_accounts(&self, ops: MergeOperation) -> Self { + let mut btree = BTreeMap::new(); + for UserToForeignAssets { account, asset_amount, asset_id } in self.iter() { + btree + .entry(account.clone()) + .and_modify(|e: &mut (BalanceOf, u32)| { + e.0 = match ops { + MergeOperation::Add => e.0.saturating_add(*asset_amount), + MergeOperation::Subtract => e.0.saturating_sub(*asset_amount), + } + }) + .or_insert((*asset_amount, *asset_id)); + } + btree.into_iter().map(|(account, info)| UserToForeignAssets::new(account, info.0, info.1)).collect() + } + + fn subtract_accounts(&self, other_list: Self) -> Self { + let current_accounts = self.accounts(); + let filtered_list = other_list.into_iter().filter(|x| current_accounts.contains(&x.account)).collect_vec(); + let mut new_list = self.clone(); + new_list.extend(filtered_list); + new_list.merge_accounts(MergeOperation::Subtract) + } + + fn sum_accounts(&self, mut other_list: Self) -> Self { + let mut output = self.clone(); + output.append(&mut other_list); + output.merge_accounts(MergeOperation::Add) + } +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "std", + serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) +)] +pub struct BidParams { + pub bidder: AccountIdOf, + pub amount: BalanceOf, + pub multiplier: MultiplierOf, + pub asset: AcceptedFundingAsset, +} +impl BidParams { + pub fn new(bidder: AccountIdOf, amount: BalanceOf, multiplier: u8, asset: AcceptedFundingAsset) -> Self { + Self { bidder, amount, multiplier: multiplier.try_into().map_err(|_| ()).unwrap(), asset } + } + + pub fn new_with_defaults(bidder: AccountIdOf, amount: BalanceOf) -> Self { + Self { + bidder, + amount, + multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), + asset: AcceptedFundingAsset::USDT, + } + } +} +impl From<(AccountIdOf, BalanceOf)> for BidParams { + fn from((bidder, amount): (AccountIdOf, BalanceOf)) -> Self { + Self { + bidder, + amount, + multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), + asset: AcceptedFundingAsset::USDT, + } + } +} +impl From<(AccountIdOf, BalanceOf, u8)> for BidParams { + fn from((bidder, amount, multiplier): (AccountIdOf, BalanceOf, u8)) -> Self { + Self { + bidder, + amount, + multiplier: multiplier.try_into().unwrap_or_else(|_| panic!("Failed to create multiplier")), + asset: AcceptedFundingAsset::USDT, + } + } +} +impl From<(AccountIdOf, BalanceOf, u8, AcceptedFundingAsset)> for BidParams { + fn from((bidder, amount, multiplier, asset): (AccountIdOf, BalanceOf, u8, AcceptedFundingAsset)) -> Self { + Self { + bidder, + amount, + multiplier: multiplier.try_into().unwrap_or_else(|_| panic!("Failed to create multiplier")), + asset, + } + } +} + +impl Accounts for Vec> { + type Account = AccountIdOf; + + fn accounts(&self) -> Vec { + let mut btree = BTreeSet::new(); + for BidParams { bidder, .. } in self { + btree.insert(bidder.clone()); + } + btree.into_iter().collect_vec() + } +} + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "std", + serde(rename_all = "camelCase", deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) +)] +pub struct ContributionParams { + pub contributor: AccountIdOf, + pub amount: BalanceOf, + pub multiplier: MultiplierOf, + pub asset: AcceptedFundingAsset, +} +impl ContributionParams { + pub fn new(contributor: AccountIdOf, amount: BalanceOf, multiplier: u8, asset: AcceptedFundingAsset) -> Self { + Self { contributor, amount, multiplier: multiplier.try_into().map_err(|_| ()).unwrap(), asset } + } + + pub fn new_with_defaults(contributor: AccountIdOf, amount: BalanceOf) -> Self { + Self { + contributor, + amount, + multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), + asset: AcceptedFundingAsset::USDT, + } + } +} +impl From<(AccountIdOf, BalanceOf)> for ContributionParams { + fn from((contributor, amount): (AccountIdOf, BalanceOf)) -> Self { + Self { + contributor, + amount, + multiplier: 1u8.try_into().unwrap_or_else(|_| panic!("multiplier could not be created from 1u8")), + asset: AcceptedFundingAsset::USDT, + } + } +} +impl From<(AccountIdOf, BalanceOf, MultiplierOf)> for ContributionParams { + fn from((contributor, amount, multiplier): (AccountIdOf, BalanceOf, MultiplierOf)) -> Self { + Self { contributor, amount, multiplier, asset: AcceptedFundingAsset::USDT } + } +} +impl From<(AccountIdOf, BalanceOf, MultiplierOf, AcceptedFundingAsset)> for ContributionParams { + fn from( + (contributor, amount, multiplier, asset): (AccountIdOf, BalanceOf, MultiplierOf, AcceptedFundingAsset), + ) -> Self { + Self { contributor, amount, multiplier, asset } + } +} +impl Accounts for Vec> { + type Account = AccountIdOf; + + fn accounts(&self) -> Vec { + let mut btree = BTreeSet::new(); + for ContributionParams { contributor, .. } in self.iter() { + btree.insert(contributor.clone()); + } + btree.into_iter().collect_vec() + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct BidInfoFilter { + pub id: Option, + pub project_id: Option, + pub bidder: Option>, + pub status: Option>>, + pub original_ct_amount: Option>, + pub original_ct_usd_price: Option>, + pub final_ct_amount: Option>, + pub final_ct_usd_price: Option>, + pub funding_asset: Option, + pub funding_asset_amount_locked: Option>, + pub multiplier: Option>, + pub plmc_bond: Option>, + pub when: Option>, +} +impl BidInfoFilter { + pub(crate) fn matches_bid(&self, bid: &BidInfoOf) -> bool { + if self.id.is_some() && self.id.unwrap() != bid.id { + return false; + } + if self.project_id.is_some() && self.project_id.unwrap() != bid.project_id { + return false; + } + if self.bidder.is_some() && self.bidder.clone().unwrap() != bid.bidder.clone() { + return false; + } + if self.status.is_some() && self.status.as_ref().unwrap() != &bid.status { + return false; + } + if self.original_ct_amount.is_some() && self.original_ct_amount.unwrap() != bid.original_ct_amount { + return false; + } + if self.original_ct_usd_price.is_some() && self.original_ct_usd_price.unwrap() != bid.original_ct_usd_price { + return false; + } + if self.final_ct_amount.is_some() && self.final_ct_amount.unwrap() != bid.final_ct_amount { + return false; + } + if self.final_ct_usd_price.is_some() && self.final_ct_usd_price.unwrap() != bid.final_ct_usd_price { + return false; + } + if self.funding_asset.is_some() && self.funding_asset.unwrap() != bid.funding_asset { + return false; + } + if self.funding_asset_amount_locked.is_some() && + self.funding_asset_amount_locked.unwrap() != bid.funding_asset_amount_locked + { + return false; + } + if self.multiplier.is_some() && self.multiplier.unwrap() != bid.multiplier { + return false; + } + if self.plmc_bond.is_some() && self.plmc_bond.unwrap() != bid.plmc_bond { + return false; + } + if self.when.is_some() && self.when.unwrap() != bid.when { + return false; + } + + true + } +} +impl Default for BidInfoFilter { + fn default() -> Self { + BidInfoFilter:: { + id: None, + project_id: None, + bidder: None, + status: None, + original_ct_amount: None, + original_ct_usd_price: None, + final_ct_amount: None, + final_ct_usd_price: None, + funding_asset: None, + funding_asset_amount_locked: None, + multiplier: None, + plmc_bond: None, + when: None, + } + } +}