From 1ef4e37281d97b00f4db4a4d0a3a87e7ba952e81 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Fri, 21 Jul 2023 14:30:34 +0200 Subject: [PATCH 1/8] feat(222): starting to write test for <= 33 funding. Need first a new PR for FinishedFunding being also a failed project feat(222): new state `AwaitingProjectDecision` wip(222) feat(220): remove outcome generation abstraction and unused evaluator reward getter function. Abstract last step in success or failed project logic from do_end_funding to use in manual acceptance/rejection --- .../pallets/funding/src/functions.rs | 237 ++++++++---------- polimec-skeleton/pallets/funding/src/lib.rs | 10 +- polimec-skeleton/pallets/funding/src/tests.rs | 60 +++++ polimec-skeleton/pallets/funding/src/types.rs | 14 +- 4 files changed, 184 insertions(+), 137 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 4912b8c22..f980a2c15 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -22,6 +22,7 @@ use super::*; use crate::traits::{BondingRequirementCalculation, ProvideStatemintPrice}; use frame_support::{ + dispatch::DispatchResult, ensure, pallet_prelude::DispatchError, traits::{ @@ -579,7 +580,6 @@ impl Pallet { let now = >::block_number(); // TODO: PLMC-149 Check if make sense to set the admin as T::fund_account_id(project_id) let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectNotFound)?; - let token_information = project_metadata.token_information; let remaining_cts = project_details.remaining_contribution_tokens; let remainder_end_block = project_details.phase_transition_points.remainder.end(); @@ -597,52 +597,43 @@ impl Pallet { .checked_mul_int(project_metadata.total_allocation_size) .ok_or(Error::::BadMath)?; let funding_reached = project_details.funding_amount_reached; - let funding_is_successful = - !(project_details.status == ProjectStatus::FundingFailed || funding_reached < funding_target); - let evaluators_outcome = Self::generate_evaluators_outcome(project_id)?; - project_details.evaluation_round_info.evaluators_outcome = evaluators_outcome; - if funding_is_successful { - project_details.status = ProjectStatus::FundingSuccessful; - project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(SuccessFinalizer::Initialized)); - - // * Update Storage * - ProjectsDetails::::insert(project_id, project_details.clone()); - T::ContributionTokenCurrency::create(project_id, project_details.issuer.clone(), false, 1_u32.into()) - .map_err(|_| Error::::AssetCreationFailed)?; - T::ContributionTokenCurrency::set( - project_id, - &project_details.issuer, - token_information.name.into(), - token_information.symbol.into(), - token_information.decimals, - ) - .map_err(|_| Error::::AssetMetadataUpdateFailed)?; + let funding_ratio = Perquintill::from_rational(funding_reached, funding_target); - // * Emit events * - let success_reason = match remaining_cts { - x if x == 0u32.into() => SuccessReason::SoldOut, - _ => SuccessReason::ReachedTarget, - }; - Self::deposit_event(Event::::FundingEnded { - project_id, - outcome: FundingOutcome::Success(success_reason), - }); + if funding_ratio <= Perquintill::from_percent(33u64) { + project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed(vec![]); + Self::make_project_funding_fail(project_id, project_details, FailureReason::TargetNotReached) + } else if funding_ratio <= Perquintill::from_percent(75u64) { + project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed(vec![]); + project_details.status = ProjectStatus::AwaitingProjectDecision; + ProjectsDetails::::insert(project_id, project_details); + Ok(()) + } else if funding_ratio < Perquintill::from_percent(90u64) { + project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Unchanged; + project_details.status = ProjectStatus::AwaitingProjectDecision; + ProjectsDetails::::insert(project_id, project_details); Ok(()) } else { - project_details.status = ProjectStatus::FundingFailed; - project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(Default::default())); + let reward_info = Self::generate_evaluator_rewards_info(project_id)?; + project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Rewarded(reward_info); + Self::make_project_funding_successful(project_id, project_details, SuccessReason::ReachedTarget) + } + } - // * Update Storage * - ProjectsDetails::::insert(project_id, project_details.clone()); + pub fn do_project_decision(project_id: T::ProjectIdentifier, decision: FundingOutcomeDecision) -> DispatchResult { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; - // * Emit events * - let failure_reason = FailureReason::TargetNotReached; - Self::deposit_event(Event::::FundingEnded { - project_id, - outcome: FundingOutcome::Failure(failure_reason), - }); - Ok(()) + // * Update storage * + match decision { + FundingOutcomeDecision::AcceptFunding => { + Self::make_project_funding_successful(project_id, project_details, SuccessReason::ProjectDecision)?; + }, + FundingOutcomeDecision::RejectFunding => { + Self::make_project_funding_fail(project_id, project_details, FailureReason::ProjectDecision)?; + }, } + + Ok(()) } /// Called manually by a user extrinsic @@ -1084,6 +1075,26 @@ impl Pallet { Ok(()) } + pub fn do_decide_project_outcome( + issuer: AccountIdOf, + project_id: T::ProjectIdentifier, + decision: FundingOutcomeDecision, + ) -> DispatchResult { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let now = >::block_number(); + + // * Validity checks * + ensure!(project_details.issuer == issuer, Error::::NotAllowed); + ensure!(project_details.status == ProjectStatus::AwaitingProjectDecision, Error::::NotAllowed); + + // * Update storage * + Self::remove_from_update_store(&project_id)?; + Self::add_to_update_store(now + 1u32.into(), (&project_id, UpdateType::ProjectDecision(decision))); + + Ok(()) + } + /// Unbond some plmc from a successful bid, after a step in the vesting period has passed. /// /// # Arguments @@ -2014,63 +2025,9 @@ impl Pallet { .fold(BalanceOf::::zero(), |acc, fee| acc.saturating_add(fee))) } - pub fn get_evaluator_ct_rewards( - project_id: T::ProjectIdentifier, - ) -> Result, DispatchError> { - let (early_evaluator_rewards, all_evaluator_rewards, early_evaluator_total_locked, all_evaluator_total_locked) = - Self::get_evaluator_rewards_info(project_id)?; - let ct_price = ProjectsDetails::::get(project_id) - .ok_or(Error::::ProjectNotFound)? - .weighted_average_price - .ok_or(Error::::ImpossibleState)?; - let evaluation_usd_amounts = Evaluations::::iter_prefix(project_id) - .map(|(evaluator, evaluations)| { - ( - evaluator, - evaluations.into_iter().fold( - (BalanceOf::::zero(), BalanceOf::::zero()), - |acc, evaluation| { - ( - acc.0.saturating_add(evaluation.early_usd_amount), - acc.1.saturating_add(evaluation.late_usd_amount), - ) - }, - ), - ) - }) - .collect::>(); - let evaluator_usd_rewards = evaluation_usd_amounts - .into_iter() - .map(|(evaluator, (early, late))| { - let early_evaluator_weight = Perquintill::from_rational(early, early_evaluator_total_locked); - let all_evaluator_weight = Perquintill::from_rational(early + late, all_evaluator_total_locked); - - let early_reward = early_evaluator_weight * early_evaluator_rewards; - let all_reward = all_evaluator_weight * all_evaluator_rewards; - - (evaluator, early_reward.saturating_add(all_reward)) - }) - .collect::>(); - let ct_price_reciprocal = ct_price.reciprocal().ok_or(Error::::BadMath)?; - - evaluator_usd_rewards - .iter() - .map(|(evaluator, usd_reward)| { - if let Some(reward) = ct_price_reciprocal.checked_mul_int(*usd_reward) { - Ok((evaluator.clone(), reward)) - } else { - Err(Error::::BadMath.into()) - } - }) - .collect() - } - - pub fn get_evaluator_rewards_info( + pub fn generate_evaluator_rewards_info( project_id: ::ProjectIdentifier, - ) -> Result< - (::Balance, ::Balance, ::Balance, ::Balance), - DispatchError, - > { + ) -> Result, DispatchError> { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; let evaluation_usd_amounts = Evaluations::::iter_prefix(project_id) .map(|(evaluator, evaluations)| { @@ -2097,49 +2054,63 @@ impl Pallet { let fees = Self::calculate_fees(project_id)?; let evaluator_fees = percentage_of_target_funding * (Perquintill::from_percent(30) * fees); - let early_evaluator_rewards = Perquintill::from_percent(20) * evaluator_fees; - let all_evaluator_rewards = Perquintill::from_percent(80) * evaluator_fees; + let early_evaluator_reward_pot_usd = Perquintill::from_percent(20) * evaluator_fees; + let normal_evaluator_reward_pot_usd = Perquintill::from_percent(80) * evaluator_fees; - let early_evaluator_total_locked = evaluation_usd_amounts + let early_evaluator_total_bonded_usd = evaluation_usd_amounts .iter() .fold(BalanceOf::::zero(), |acc, (_, (early, _))| acc.saturating_add(*early)); - let late_evaluator_total_locked = + let late_evaluator_total_bonded_usd = evaluation_usd_amounts.iter().fold(BalanceOf::::zero(), |acc, (_, (_, late))| acc.saturating_add(*late)); - let all_evaluator_total_locked = early_evaluator_total_locked.saturating_add(late_evaluator_total_locked); - Ok((early_evaluator_rewards, all_evaluator_rewards, early_evaluator_total_locked, all_evaluator_total_locked)) + let normal_evaluator_total_bonded_usd = + early_evaluator_total_bonded_usd.saturating_add(late_evaluator_total_bonded_usd); + Ok(RewardInfo { + early_evaluator_reward_pot_usd, + normal_evaluator_reward_pot_usd, + early_evaluator_total_bonded_usd, + normal_evaluator_total_bonded_usd, + }) } - pub fn generate_evaluators_outcome( + pub fn make_project_funding_successful( project_id: T::ProjectIdentifier, - ) -> Result, DispatchError> { - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; - let funding_target = project_details.fundraising_target; - let funding_reached = project_details.funding_amount_reached; - let funding_ratio = Perquintill::from_rational(funding_reached, funding_target); + mut project_details: ProjectDetailsOf, + reason: SuccessReason, + ) -> DispatchResult { + let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let token_information = project_metadata.token_information; - // Project Automatically rejected, evaluators slashed - if funding_ratio <= Perquintill::from_percent(33) { - todo!() - // Project Manually accepted, evaluators slashed - } else if funding_ratio < Perquintill::from_percent(75) { - todo!() - // Project Manually accepted, evaluators unaffected - } else if funding_ratio < Perquintill::from_percent(90) { - todo!() - // Project Automatically accepted, evaluators rewarded - } else { - let ( - early_evaluator_reward_pot_usd, - normal_evaluator_reward_pot_usd, - early_evaluator_total_bonded_usd, - normal_evaluator_total_bonded_usd, - ) = Self::get_evaluator_rewards_info(project_id)?; - Ok(EvaluatorsOutcome::Rewarded(RewardInfo { - early_evaluator_reward_pot_usd, - normal_evaluator_reward_pot_usd, - early_evaluator_total_bonded_usd, - normal_evaluator_total_bonded_usd, - })) - } + project_details.status = ProjectStatus::FundingSuccessful; + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(SuccessFinalizer::Initialized)); + + ProjectsDetails::::insert(project_id, project_details.clone()); + T::ContributionTokenCurrency::create(project_id, project_details.issuer.clone(), false, 1_u32.into()) + .map_err(|_| Error::::AssetCreationFailed)?; + T::ContributionTokenCurrency::set( + project_id, + &project_details.issuer, + token_information.name.into(), + token_information.symbol.into(), + token_information.decimals, + ) + .map_err(|_| Error::::AssetMetadataUpdateFailed)?; + + Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Success(reason) }); + + Ok(()) + } + + pub fn make_project_funding_fail( + project_id: T::ProjectIdentifier, + mut project_details: ProjectDetailsOf, + reason: FailureReason, + ) -> DispatchResult { + project_details.status = ProjectStatus::FundingFailed; + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(Default::default())); + + ProjectsDetails::::insert(project_id, project_details.clone()); + + Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Failure(reason) }); + Ok(()) } } diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index f255d1fd3..98df9deaf 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -210,6 +210,7 @@ use frame_support::{ use parity_scale_codec::{Decode, Encode}; use sp_arithmetic::traits::{One, Saturating}; + use sp_runtime::{traits::AccountIdConversion, FixedPointNumber, FixedPointOperand, FixedU128}; use sp_std::prelude::*; @@ -225,6 +226,7 @@ pub type HashOf = ::Hash; pub type AssetIdOf = <::FundingCurrency as fungibles::Inspect<::AccountId>>::AssetId; +pub type RewardInfoOf = RewardInfo>; pub type EvaluatorsOutcomeOf = EvaluatorsOutcome, BalanceOf>; pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; @@ -313,8 +315,6 @@ pub mod pallet { /// Something that provides the members of Polimec type HandleMembers: PolimecMembers>; - type Vesting: polimec_traits::ReleaseSchedule, BondTypeOf>; - /// The maximum length of data stored on-chain. #[pallet::constant] type StringLimit: Get; @@ -381,6 +381,8 @@ pub mod pallet { type FeeBrackets: Get>; type EvaluationSuccessThreshold: Get; + + type Vesting: polimec_traits::ReleaseSchedule, BondTypeOf>; } #[pallet::storage] @@ -912,6 +914,10 @@ pub mod pallet { UpdateType::FundingEnd => { unwrap_result_or_skip!(Self::do_end_funding(project_id), project_id) }, + + UpdateType::ProjectDecision(decision) => { + unwrap_result_or_skip!(Self::do_project_decision(project_id, decision), project_id) + }, } } // TODO: PLMC-127. Set a proper weight diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index de2e8a752..0771c77fb 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -221,6 +221,7 @@ const BIDDER_1: AccountId = 30; const BIDDER_2: AccountId = 31; const BIDDER_3: AccountId = 32; const BIDDER_4: AccountId = 33; +const BIDDER_5: AccountId = 34; const BUYER_1: AccountId = 40; const BUYER_2: AccountId = 41; const BUYER_3: AccountId = 42; @@ -1242,6 +1243,7 @@ mod defaults { pub mod helper_functions { use super::*; use sp_arithmetic::traits::Zero; + use sp_arithmetic::Percent; use sp_core::H256; use std::collections::BTreeMap; @@ -1557,6 +1559,38 @@ pub mod helper_functions { _ => Ok(()), } } + + pub fn generate_bids_from_total_usd( + usd_amount: BalanceOf, min_price: PriceOf, + ) -> TestBids { + const WEIGHTS: [u8; 5] = [30u8, 20u8, 15u8, 10u8, 25u8]; + const BIDDERS: [AccountIdOf; 5] = [BUYER_1, BUYER_2, BUYER_3, BUYER_4, BUYER_5]; + + zip(WEIGHTS, BIDDERS) + .map(|(weight, bidder)| { + let ticket_size = Percent::from_percent(weight) * usd_amount; + let token_amount = min_price.reciprocal().unwrap().saturating_mul_int(ticket_size); + + TestBid::new(bidder, token_amount, min_price, None, AcceptedFundingAsset::USDT) + }) + .collect() + } + + pub fn generate_contributions_from_total_usd( + usd_amount: BalanceOf, final_price: PriceOf, + ) -> TestContributions { + const WEIGHTS: [u8; 5] = [30u8, 20u8, 15u8, 10u8, 25u8]; + const BIDDERS: [AccountIdOf; 5] = [BIDDER_1, BIDDER_2, BIDDER_3, BIDDER_4, BIDDER_5]; + + zip(WEIGHTS, BIDDERS) + .map(|(weight, bidder)| { + let ticket_size = Percent::from_percent(weight) * usd_amount; + let token_amount = final_price.reciprocal().unwrap().saturating_mul_int(ticket_size); + + TestContribution::new(bidder, token_amount, None, AcceptedFundingAsset::USDT) + }) + .collect() + } } #[cfg(test)] @@ -3382,6 +3416,32 @@ mod remainder_round_success { } } +#[cfg(test)] +mod funding_end { + use super::*; + use sp_arithmetic::{Percent, Perquintill}; + + #[test] + fn automatic_fail_less_eq_33_percent() { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(20u64) + * (project_metadata + .minimum_price + .checked_mul_int(project_metadata.total_allocation_size) + .unwrap()); + let evaluations = default_evaluations(); + let bids = generate_bids_from_total_usd(Percent::from_percent(10u8) * twenty_percent_funding_usd, min_price); + let contributions = + generate_contributions_from_total_usd(Percent::from_percent(10u8) * twenty_percent_funding_usd, min_price); + let remainder_project = + RemainderFundingProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids, contributions); + + remainder_project.end_funding(); + } +} + #[cfg(test)] mod purchased_vesting { use super::*; diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 189e2a8ef..9928bd992 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -141,7 +141,7 @@ pub mod storage_types { } /// Tells on_initialize what to do with the project - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Copy, Ord, PartialOrd)] + #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum UpdateType { EvaluationEnd, EnglishAuctionStart, @@ -149,6 +149,7 @@ pub mod storage_types { CommunityFundingStart, RemainderFundingStart, FundingEnd, + ProjectDecision(FundingOutcomeDecision), } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Ord, PartialOrd)] @@ -345,8 +346,9 @@ pub mod inner_types { AuctionRound(AuctionPhase), CommunityRound, RemainderRound, - FundingSuccessful, FundingFailed, + AwaitingProjectDecision, + FundingSuccessful, ReadyToLaunch, } @@ -475,6 +477,7 @@ pub mod inner_types { pub enum SuccessReason { SoldOut, ReachedTarget, + ProjectDecision, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -482,6 +485,7 @@ pub mod inner_types { EvaluationFailed, AuctionFailed, TargetNotReached, + ProjectDecision, Unknown, } @@ -549,4 +553,10 @@ pub mod inner_types { pub early_evaluator_total_bonded_usd: Balance, pub normal_evaluator_total_bonded_usd: Balance, } + + #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum FundingOutcomeDecision { + AcceptFunding, + RejectFunding, + } } From 72a53af5cc24f9f0b56463fc0d9aeaf025c8a61e Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 24 Jul 2023 15:12:34 +0200 Subject: [PATCH 2/8] chore(222): fmt --- polimec-skeleton/pallets/funding/src/tests.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 0771c77fb..56e6a7f45 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1242,8 +1242,7 @@ mod defaults { pub mod helper_functions { use super::*; - use sp_arithmetic::traits::Zero; - use sp_arithmetic::Percent; + use sp_arithmetic::{traits::Zero, Percent}; use sp_core::H256; use std::collections::BTreeMap; @@ -1561,7 +1560,8 @@ pub mod helper_functions { } pub fn generate_bids_from_total_usd( - usd_amount: BalanceOf, min_price: PriceOf, + usd_amount: BalanceOf, + min_price: PriceOf, ) -> TestBids { const WEIGHTS: [u8; 5] = [30u8, 20u8, 15u8, 10u8, 25u8]; const BIDDERS: [AccountIdOf; 5] = [BUYER_1, BUYER_2, BUYER_3, BUYER_4, BUYER_5]; @@ -1577,7 +1577,8 @@ pub mod helper_functions { } pub fn generate_contributions_from_total_usd( - usd_amount: BalanceOf, final_price: PriceOf, + usd_amount: BalanceOf, + final_price: PriceOf, ) -> TestContributions { const WEIGHTS: [u8; 5] = [30u8, 20u8, 15u8, 10u8, 25u8]; const BIDDERS: [AccountIdOf; 5] = [BIDDER_1, BIDDER_2, BIDDER_3, BIDDER_4, BIDDER_5]; @@ -3426,11 +3427,8 @@ mod funding_end { let test_env = TestEnvironment::new(); let project_metadata = default_project(test_env.get_new_nonce()); let min_price = project_metadata.minimum_price; - let twenty_percent_funding_usd = Perquintill::from_percent(20u64) - * (project_metadata - .minimum_price - .checked_mul_int(project_metadata.total_allocation_size) - .unwrap()); + let twenty_percent_funding_usd = Perquintill::from_percent(20u64) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); let evaluations = default_evaluations(); let bids = generate_bids_from_total_usd(Percent::from_percent(10u8) * twenty_percent_funding_usd, min_price); let contributions = From 8a9dea2c72d356a66c987bd46a1f0ff6afa160d6 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 24 Jul 2023 15:12:42 +0200 Subject: [PATCH 3/8] chore(222): fmt --- polimec-skeleton/pallets/linear-release/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/polimec-skeleton/pallets/linear-release/src/lib.rs b/polimec-skeleton/pallets/linear-release/src/lib.rs index f20c34149..1e576cd24 100644 --- a/polimec-skeleton/pallets/linear-release/src/lib.rs +++ b/polimec-skeleton/pallets/linear-release/src/lib.rs @@ -50,7 +50,6 @@ mod tests; mod impls; mod types; - // TODO: Find a way to use // 1. type BalanceOf = <::Currency as fungible::Inspect<::AccountId>>::Balance; // 2. type ReasonOf = <::Currency as InspectHold<::AccountId>>::Reason; From fac8767687f8f1c543316297e34eaef5c2666387 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 24 Jul 2023 16:07:26 +0200 Subject: [PATCH 4/8] feat(222): `RemainderFundingProject::new_with()` now returns `Either` remainder, or finished project. --- polimec-skeleton/pallets/funding/src/tests.rs | 160 ++++++++++++------ 1 file changed, 112 insertions(+), 48 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 56e6a7f45..602f99711 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -40,7 +40,7 @@ use helper_functions::*; use crate::traits::BondingRequirementCalculation; use sp_arithmetic::traits::Zero; -use sp_runtime::DispatchError; +use sp_runtime::{DispatchError, Either}; use std::{cell::RefCell, iter::zip}; type ProjectIdOf = ::ProjectIdentifier; @@ -915,7 +915,7 @@ impl<'a> CommunityFundingProject<'a> { ); } - fn start_remainder_funding(self) -> RemainderFundingProject<'a> { + fn start_remainder_or_end_funding(self) -> Either, FinishedProject<'a>> { assert_eq!(self.get_project_details().status, ProjectStatus::CommunityRound); let community_funding_end = self .get_project_details() @@ -925,8 +925,19 @@ impl<'a> CommunityFundingProject<'a> { .expect("Community funding end point should exist"); let remainder_start = community_funding_end + 1; self.test_env.advance_time(remainder_start.saturating_sub(self.test_env.current_block())).unwrap(); - assert_eq!(self.get_project_details().status, ProjectStatus::RemainderRound); - RemainderFundingProject { test_env: self.test_env, issuer: self.issuer, project_id: self.project_id } + match self.get_project_details().status { + ProjectStatus::RemainderRound => Either::Left(RemainderFundingProject { + test_env: self.test_env, + issuer: self.issuer, + project_id: self.project_id, + }), + ProjectStatus::FundingSuccessful => Either::Right(FinishedProject { + test_env: self.test_env, + issuer: self.issuer, + project_id: self.project_id, + }), + _ => panic!("Unknown state"), + } } fn finish_funding(self) -> FinishedProject<'a> { @@ -990,7 +1001,7 @@ impl<'a> RemainderFundingProject<'a> { evaluations: UserToUSDBalance, bids: TestBids, contributions: TestContributions, - ) -> Self { + ) -> Either { let community_funding_project = CommunityFundingProject::new_with(test_env, project_metadata, issuer, evaluations.clone(), bids.clone()); @@ -1035,19 +1046,19 @@ impl<'a> RemainderFundingProject<'a> { test_env.do_free_statemint_asset_assertions(prev_funding_asset_balances); test_env.do_total_plmc_assertions(post_supply); - community_funding_project.start_remainder_funding() + community_funding_project.start_remainder_or_end_funding() } - // 227_683_3_333_333_330 - // 1_000_000_0_000_000_000 - fn end_funding(&self) -> FinishedProject<'a> { assert_eq!(self.get_project_details().status, ProjectStatus::RemainderRound); let remainder_funding_end = self.get_project_details().phase_transition_points.remainder.end().expect("Should have remainder end"); let finish_block = remainder_funding_end + 1; self.test_env.advance_time(finish_block.saturating_sub(self.test_env.current_block())).unwrap(); - assert_eq!(self.get_project_details().status, ProjectStatus::FundingSuccessful); + assert!(matches!( + self.get_project_details().status, + ProjectStatus::FundingSuccessful | ProjectStatus::FundingFailed | ProjectStatus::AwaitingProjectDecision + )); FinishedProject { test_env: self.test_env, issuer: self.issuer.clone(), project_id: self.project_id.clone() } } @@ -1082,7 +1093,7 @@ impl<'a> FinishedProject<'a> { community_contributions: TestContributions, remainder_contributions: TestContributions, ) -> Self { - let remainder_funding_project = RemainderFundingProject::new_with( + let project = RemainderFundingProject::new_with( test_env, project_metadata.clone(), issuer, @@ -1091,9 +1102,12 @@ impl<'a> FinishedProject<'a> { community_contributions.clone(), ); - if remainder_contributions.is_empty() { - return remainder_funding_project.end_funding() - } + let remainder_funding_project = match project { + Either::Right(finished_project) => return finished_project, + Either::Left(remainder_project) if remainder_contributions.is_empty() => + return remainder_project.end_funding(), + Either::Left(remainder_project) => remainder_project, + }; let project_id = remainder_funding_project.get_project_id(); let ct_price = remainder_funding_project.get_project_details().weighted_average_price.unwrap(); @@ -1149,20 +1163,22 @@ impl<'a> FinishedProject<'a> { let finished_project = remainder_funding_project.end_funding(); - // Check that remaining CTs are updated - let project_details = finished_project.get_project_details(); - let auction_bought_tokens: u128 = bids.iter().map(|bid| bid.amount).sum(); - let community_bought_tokens: u128 = community_contributions.iter().map(|cont| cont.amount).sum(); - let remainder_bought_tokens: u128 = remainder_contributions.iter().map(|cont| cont.amount).sum(); + if finished_project.get_project_details().status == ProjectStatus::FundingSuccessful { + // Check that remaining CTs are updated + let project_details = finished_project.get_project_details(); + let auction_bought_tokens: u128 = bids.iter().map(|bid| bid.amount).sum(); + let community_bought_tokens: u128 = community_contributions.iter().map(|cont| cont.amount).sum(); + let remainder_bought_tokens: u128 = remainder_contributions.iter().map(|cont| cont.amount).sum(); - 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" - ); + 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" + ); + } finished_project } @@ -1860,7 +1876,8 @@ mod evaluation_round_success { evaluations.clone(), default_bids(), default_community_buys(), - ); + ) + .unwrap_left(); let project_id = remainder_funding_project.get_project_id(); let prev_reserved_plmc = test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); @@ -1895,7 +1912,8 @@ mod evaluation_round_success { evaluations.clone(), vec![TestBid::new(BUYER_1, 1000 * ASSET_UNIT, 10u128.into(), None, AcceptedFundingAsset::USDT)], vec![TestContribution::new(BUYER_1, 1000 * US_DOLLAR, None, AcceptedFundingAsset::USDT)], - ); + ) + .unwrap_left(); let project_id = remainder_funding_project.get_project_id(); let prev_reserved_plmc = @@ -3209,9 +3227,10 @@ mod remainder_round_success { evaluations.push((evaluator_contributor, evaluation_amount)); let bids = default_bids(); - let contributing_project = - RemainderFundingProject::new_with(&test_env, project, issuer, evaluations, bids, community_contributions); - let ct_price = contributing_project.get_project_details().weighted_average_price.unwrap(); + let remainder_funding_project = + RemainderFundingProject::new_with(&test_env, project, issuer, evaluations, bids, community_contributions) + .unwrap_left(); + let ct_price = remainder_funding_project.get_project_details().weighted_average_price.unwrap(); let already_bonded_plmc = calculate_evaluation_plmc_spent(vec![(evaluator_contributor, evaluation_amount)])[0].1; let necessary_plmc_for_buy = calculate_contributed_plmc_spent(vec![remainder_contribution], ct_price)[0].1; @@ -3220,7 +3239,7 @@ mod remainder_round_success { test_env.mint_plmc_to(vec![(evaluator_contributor, necessary_plmc_for_buy - already_bonded_plmc)]); test_env.mint_statemint_asset_to(necessary_usdt_for_buy); - contributing_project.buy_for_any_user(vec![remainder_contribution]).unwrap(); + remainder_funding_project.buy_for_any_user(vec![remainder_contribution]).unwrap(); } #[test] @@ -3263,7 +3282,8 @@ mod remainder_round_success { evaluations.push((evaluator_contributor, evaluation_usd_amount)); let remainder_funding_project = - RemainderFundingProject::new_with(&test_env, project, issuer, evaluations, bids, default_community_buys()); + RemainderFundingProject::new_with(&test_env, project, issuer, evaluations, bids, default_community_buys()) + .unwrap_left(); let project_id = remainder_funding_project.get_project_id(); test_env.mint_plmc_to(vec![(evaluator_contributor, FUNDED_DELTA_PLMC)]); @@ -3304,7 +3324,8 @@ mod remainder_round_success { default_evaluations(), default_bids(), default_community_buys(), - ); + ) + .unwrap_left(); const BOB: AccountId = 808; let remaining_ct = remainder_funding_project.get_project_details().remaining_contribution_tokens; @@ -3357,7 +3378,8 @@ mod remainder_round_success { default_evaluations(), default_bids(), default_community_buys(), - ); + ) + .unwrap_left(); const BOB: AccountId = 808; const OVERBUY_CT: BalanceOf = 40 * ASSET_UNIT; @@ -3424,20 +3446,62 @@ mod funding_end { #[test] fn automatic_fail_less_eq_33_percent() { - let test_env = TestEnvironment::new(); - let project_metadata = default_project(test_env.get_new_nonce()); - let min_price = project_metadata.minimum_price; - let twenty_percent_funding_usd = Perquintill::from_percent(20u64) * - (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); - let evaluations = default_evaluations(); - let bids = generate_bids_from_total_usd(Percent::from_percent(10u8) * twenty_percent_funding_usd, min_price); - let contributions = - generate_contributions_from_total_usd(Percent::from_percent(10u8) * twenty_percent_funding_usd, min_price); - let remainder_project = - RemainderFundingProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids, contributions); + for funding_percent in 1..=33 { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(funding_percent) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); + let evaluations = default_evaluations(); + let bids = + generate_bids_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let contributions = generate_contributions_from_total_usd( + Percent::from_percent(50u8) * twenty_percent_funding_usd, + min_price, + ); + let finished_project = FinishedProject::new_with( + &test_env, + project_metadata, + ISSUER, + evaluations, + bids, + contributions, + vec![], + ); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); + } + } - remainder_project.end_funding(); + #[test] + fn automatic_success_bigger_eq_90_percent() { + for funding_percent in 90..=100 { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(funding_percent) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); + let evaluations = default_evaluations(); + let bids = + generate_bids_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let contributions = generate_contributions_from_total_usd( + Percent::from_percent(50u8) * twenty_percent_funding_usd, + min_price, + ); + let finished_project = FinishedProject::new_with( + &test_env, + project_metadata, + ISSUER, + evaluations, + bids, + contributions, + vec![], + ); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingSuccessful); + } } + + #[test] + fn manual_outcome_above33_to_below90() {} } #[cfg(test)] From 9afd5c500158b3206dbbdef8c80372689830254c Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 24 Jul 2023 16:46:54 +0200 Subject: [PATCH 5/8] feat(222): AwaitingProjectDecision state now tested --- polimec-skeleton/pallets/funding/src/tests.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 602f99711..859a44411 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -3501,7 +3501,32 @@ mod funding_end { } #[test] - fn manual_outcome_above33_to_below90() {} + fn manual_outcome_above33_to_below90() { + for funding_percent in 34..90 { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(funding_percent) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); + let evaluations = default_evaluations(); + let bids = + generate_bids_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let contributions = generate_contributions_from_total_usd( + Percent::from_percent(50u8) * twenty_percent_funding_usd, + min_price, + ); + let finished_project = FinishedProject::new_with( + &test_env, + project_metadata, + ISSUER, + evaluations, + bids, + contributions, + vec![], + ); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); + } + } } #[cfg(test)] From f107747644cfc342284c688389e663d7e3b58651 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 24 Jul 2023 17:08:55 +0200 Subject: [PATCH 6/8] feat(222): manual acceptance and rejection tested. done by on_initialize --- .../pallets/funding/src/functions.rs | 1 - polimec-skeleton/pallets/funding/src/tests.rs | 54 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index f980a2c15..bda2d8be7 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -1089,7 +1089,6 @@ impl Pallet { ensure!(project_details.status == ProjectStatus::AwaitingProjectDecision, Error::::NotAllowed); // * Update storage * - Self::remove_from_update_store(&project_id)?; Self::add_to_update_store(now + 1u32.into(), (&project_id, UpdateType::ProjectDecision(decision))); Ok(()) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 859a44411..5f0495841 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -3527,6 +3527,60 @@ mod funding_end { assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); } } + + #[test] + fn manual_acceptance() { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(55) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); + let evaluations = default_evaluations(); + let bids = generate_bids_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let contributions = + generate_contributions_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let finished_project = + FinishedProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids, contributions, vec![]); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); + + let project_id = finished_project.project_id; + test_env + .in_ext(|| { + FundingModule::do_decide_project_outcome(ISSUER, project_id, FundingOutcomeDecision::AcceptFunding) + }) + .unwrap(); + + test_env.advance_time(2u64).unwrap(); + + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingSuccessful); + } + + #[test] + fn manual_rejection() { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(55) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); + let evaluations = default_evaluations(); + let bids = generate_bids_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let contributions = + generate_contributions_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let finished_project = + FinishedProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids, contributions, vec![]); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); + + let project_id = finished_project.project_id; + test_env + .in_ext(|| { + FundingModule::do_decide_project_outcome(ISSUER, project_id, FundingOutcomeDecision::RejectFunding) + }) + .unwrap(); + + test_env.advance_time(2u64).unwrap(); + + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); + } } #[cfg(test)] From 6e218df0dc002ac6f3caf739df2121e6d2316692 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Tue, 25 Jul 2023 14:55:04 +0200 Subject: [PATCH 7/8] feat(222): finalizer checked. Move to nightly compiler to use assert_matches feature --- polimec-skeleton/pallets/funding/src/lib.rs | 1 + polimec-skeleton/pallets/funding/src/tests.rs | 53 ++++++++++++++++++- polimec-skeleton/rust-toolchain.toml | 2 +- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 98df9deaf..91a13d9b0 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -176,6 +176,7 @@ // This recursion limit is needed because we have too many benchmarks and benchmarking will fail if // we add more without this limit. #![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "512")] +#![feature(assert_matches)] pub mod functions; pub mod types; diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 5f0495841..8e4843ca5 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -18,6 +18,8 @@ //! Tests for Funding pallet. +#![feature(assert_matches)] + use super::*; use crate as pallet_funding; use crate::{ @@ -1258,6 +1260,9 @@ mod defaults { pub mod helper_functions { use super::*; + use frame_support::traits::fungibles::{ + metadata::Inspect as MetadataInspect, roles::Inspect as RolesInspect, Inspect, + }; use sp_arithmetic::{traits::Zero, Percent}; use sp_core::H256; use std::collections::BTreeMap; @@ -1608,6 +1613,32 @@ pub mod helper_functions { }) .collect() } + + pub fn test_ct_created_for(test_env: &TestEnvironment, project_id: ProjectIdOf) { + test_env.in_ext(|| { + let metadata = ProjectsMetadata::::get(project_id).unwrap(); + let details = ProjectsDetails::::get(project_id).unwrap(); + assert_eq!( + ::ContributionTokenCurrency::name(project_id), + metadata.token_information.name.to_vec() + ); + assert_eq!(::ContributionTokenCurrency::admin(project_id).unwrap(), details.issuer); + assert_eq!( + ::ContributionTokenCurrency::total_issuance(project_id), + 0u32.into(), + "No CTs should have been minted at this point" + ); + }); + } + + pub fn test_ct_not_created_for(test_env: &TestEnvironment, project_id: ProjectIdOf) { + test_env.in_ext(|| { + assert!( + !::ContributionTokenCurrency::asset_exists(project_id), + "Asset shouldn't exist, since funding failed" + ); + }); + } } #[cfg(test)] @@ -3443,6 +3474,7 @@ mod remainder_round_success { mod funding_end { use super::*; use sp_arithmetic::{Percent, Perquintill}; + use std::assert_matches::assert_matches; #[test] fn automatic_fail_less_eq_33_percent() { @@ -3550,9 +3582,17 @@ mod funding_end { }) .unwrap(); - test_env.advance_time(2u64).unwrap(); + test_env.advance_time(1u64).unwrap(); assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingSuccessful); + assert_matches!( + finished_project.get_project_details().cleanup, + ProjectCleanup::Ready(ProjectFinalizer::Success(_)), + ); + test_ct_created_for(&test_env, project_id); + + test_env.advance_time(10u64).unwrap(); + assert_matches!(finished_project.get_project_details().cleanup, ProjectCleanup::Finished,); } #[test] @@ -3577,9 +3617,18 @@ mod funding_end { }) .unwrap(); - test_env.advance_time(2u64).unwrap(); + test_env.advance_time(1u64).unwrap(); assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); + assert_matches!( + finished_project.get_project_details().cleanup, + ProjectCleanup::Ready(ProjectFinalizer::Failure(_)) + ); + + test_ct_not_created_for(&test_env, project_id); + + test_env.advance_time(1u64).unwrap(); + assert_matches!(finished_project.get_project_details().cleanup, ProjectCleanup::Finished); } } diff --git a/polimec-skeleton/rust-toolchain.toml b/polimec-skeleton/rust-toolchain.toml index 6f5d27ec8..3b963a94f 100644 --- a/polimec-skeleton/rust-toolchain.toml +++ b/polimec-skeleton/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "stable" +channel = "nightly" components = [ "rustfmt", "clippy" ] targets = [ "wasm32-unknown-unknown" ] \ No newline at end of file From 74a1141f03bfe089250122e840b60c11d9a48ba9 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Tue, 25 Jul 2023 16:36:07 +0200 Subject: [PATCH 8/8] feat(222): automatic acceptance and time delay for finalization after success --- .../pallets/funding/src/functions.rs | 96 ++++++++++++++----- polimec-skeleton/pallets/funding/src/impls.rs | 10 +- polimec-skeleton/pallets/funding/src/lib.rs | 8 ++ polimec-skeleton/pallets/funding/src/mock.rs | 4 + polimec-skeleton/pallets/funding/src/tests.rs | 39 +++++++- polimec-skeleton/pallets/funding/src/types.rs | 3 +- polimec-skeleton/runtime/src/lib.rs | 14 +++ 7 files changed, 140 insertions(+), 34 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index bda2d8be7..9ffa62c3c 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -577,11 +577,11 @@ impl Pallet { pub fn do_end_funding(project_id: T::ProjectIdentifier) -> Result<(), DispatchError> { // * Get variables * let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; - let now = >::block_number(); // TODO: PLMC-149 Check if make sense to set the admin as T::fund_account_id(project_id) let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectNotFound)?; let remaining_cts = project_details.remaining_contribution_tokens; let remainder_end_block = project_details.phase_transition_points.remainder.end(); + let now = >::block_number(); // * Validity checks * ensure!( @@ -599,12 +599,17 @@ impl Pallet { let funding_reached = project_details.funding_amount_reached; let funding_ratio = Perquintill::from_rational(funding_reached, funding_target); + // * Update Storage * if funding_ratio <= Perquintill::from_percent(33u64) { project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed(vec![]); - Self::make_project_funding_fail(project_id, project_details, FailureReason::TargetNotReached) + Self::make_project_funding_fail(project_id, project_details, FailureReason::TargetNotReached, 0u32.into()) } else if funding_ratio <= Perquintill::from_percent(75u64) { project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed(vec![]); project_details.status = ProjectStatus::AwaitingProjectDecision; + Self::add_to_update_store( + now + T::ManualAcceptanceDuration::get() + 1u32.into(), + (&project_id, UpdateType::ProjectDecision(FundingOutcomeDecision::AcceptFunding)), + ); ProjectsDetails::::insert(project_id, project_details); Ok(()) } else if funding_ratio < Perquintill::from_percent(90u64) { @@ -615,7 +620,12 @@ impl Pallet { } else { let reward_info = Self::generate_evaluator_rewards_info(project_id)?; project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Rewarded(reward_info); - Self::make_project_funding_successful(project_id, project_details, SuccessReason::ReachedTarget) + Self::make_project_funding_successful( + project_id, + project_details, + SuccessReason::ReachedTarget, + 0u32.into(), + ) } } @@ -626,16 +636,59 @@ impl Pallet { // * Update storage * match decision { FundingOutcomeDecision::AcceptFunding => { - Self::make_project_funding_successful(project_id, project_details, SuccessReason::ProjectDecision)?; + Self::make_project_funding_successful( + project_id, + project_details, + SuccessReason::ProjectDecision, + T::SuccessToSettlementTime::get(), + )?; }, FundingOutcomeDecision::RejectFunding => { - Self::make_project_funding_fail(project_id, project_details, FailureReason::ProjectDecision)?; + Self::make_project_funding_fail( + project_id, + project_details, + FailureReason::ProjectDecision, + T::SuccessToSettlementTime::get(), + )?; }, } Ok(()) } + pub fn do_start_settlement(project_id: T::ProjectIdentifier, finalizer: ProjectFinalizer) -> DispatchResult { + // * Get variables * + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let token_information = + ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectNotFound)?.token_information; + + // * Validity checks * + ensure!( + project_details.status == ProjectStatus::FundingSuccessful || + project_details.status == ProjectStatus::FundingFailed, + Error::::NotAllowed + ); + + // * Update storage * + project_details.cleanup = ProjectCleanup::Ready(finalizer); + ProjectsDetails::::insert(project_id, project_details.clone()); + + if project_details.status == ProjectStatus::FundingSuccessful { + T::ContributionTokenCurrency::create(project_id, project_details.issuer.clone(), false, 1_u32.into()) + .map_err(|_| Error::::AssetCreationFailed)?; + T::ContributionTokenCurrency::set( + project_id, + &project_details.issuer, + token_information.name.into(), + token_information.symbol.into(), + token_information.decimals, + ) + .map_err(|_| Error::::AssetMetadataUpdateFailed)?; + } + + Ok(()) + } + /// Called manually by a user extrinsic /// Marks the project as ready to launch on mainnet, which will in the future start the logic /// to burn the contribution tokens and mint the real tokens the project's chain @@ -1089,6 +1142,7 @@ impl Pallet { ensure!(project_details.status == ProjectStatus::AwaitingProjectDecision, Error::::NotAllowed); // * Update storage * + Self::remove_from_update_store(&project_id)?; Self::add_to_update_store(now + 1u32.into(), (&project_id, UpdateType::ProjectDecision(decision))); Ok(()) @@ -1556,7 +1610,7 @@ impl Pallet { // Try to get the project into the earliest possible block to update. // There is a limit for how many projects can update each block, so we need to make sure we don't exceed that limit let mut block_number = block_number; - while ProjectsToUpdate::::try_append(block_number, store).is_err() { + while ProjectsToUpdate::::try_append(block_number, store.clone()).is_err() { // TODO: Should we end the loop if we iterated over too many blocks? block_number += 1u32.into(); } @@ -2075,24 +2129,16 @@ impl Pallet { project_id: T::ProjectIdentifier, mut project_details: ProjectDetailsOf, reason: SuccessReason, + settlement_delta: T::BlockNumber, ) -> DispatchResult { - let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectNotFound)?; - let token_information = project_metadata.token_information; - + let now = >::block_number(); project_details.status = ProjectStatus::FundingSuccessful; - project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(SuccessFinalizer::Initialized)); - ProjectsDetails::::insert(project_id, project_details.clone()); - T::ContributionTokenCurrency::create(project_id, project_details.issuer.clone(), false, 1_u32.into()) - .map_err(|_| Error::::AssetCreationFailed)?; - T::ContributionTokenCurrency::set( - project_id, - &project_details.issuer, - token_information.name.into(), - token_information.symbol.into(), - token_information.decimals, - ) - .map_err(|_| Error::::AssetMetadataUpdateFailed)?; + + Self::add_to_update_store( + now + settlement_delta, + (&project_id, UpdateType::StartSettlement(ProjectFinalizer::Success(SuccessFinalizer::Initialized))), + ); Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Success(reason) }); @@ -2103,12 +2149,16 @@ impl Pallet { project_id: T::ProjectIdentifier, mut project_details: ProjectDetailsOf, reason: FailureReason, + settlement_delta: T::BlockNumber, ) -> DispatchResult { + let now = >::block_number(); project_details.status = ProjectStatus::FundingFailed; - project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(Default::default())); - ProjectsDetails::::insert(project_id, project_details.clone()); + Self::add_to_update_store( + now + settlement_delta, + (&project_id, UpdateType::StartSettlement(ProjectFinalizer::Failure(FailureFinalizer::Initialized))), + ); Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Failure(reason) }); Ok(()) } diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 201b33d33..1f8645905 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -285,7 +285,7 @@ fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) - .find(|evaluations| evaluations.iter().any(|evaluation| !evaluation.rewarded_or_slashed)); if let Some(mut user_evaluations) = maybe_user_evaluations { - let mut evaluation = user_evaluations + let evaluation = user_evaluations .iter_mut() .find(|evaluation| !evaluation.rewarded_or_slashed) .expect("user_evaluations can only exist if an item here is found; qed"); @@ -360,7 +360,7 @@ fn release_funds_one_bid(project_id: T::ProjectIdentifier) -> (Weight let maybe_user_bids = project_bids.into_iter().find(|bids| bids.iter().any(|bid| !bid.funds_released)); if let Some(mut user_bids) = maybe_user_bids { - let mut bid = user_bids + let bid = user_bids .iter_mut() .find(|bid| !bid.funds_released) .expect("user_bids can only exist if an item here is found; qed"); @@ -438,7 +438,7 @@ fn release_funds_one_contribution(project_id: T::ProjectIdentifier) - .find(|contributions| contributions.iter().any(|contribution| !contribution.funds_released)); if let Some(mut user_contributions) = maybe_user_contributions { - let mut contribution = user_contributions + let contribution = user_contributions .iter_mut() .find(|contribution| !contribution.funds_released) .expect("user_contributions can only exist if an item here is found; qed"); @@ -536,7 +536,7 @@ fn issuer_funding_payout_one_bid(project_id: T::ProjectIdentifier) -> let maybe_user_bids = project_bids.into_iter().find(|bids| bids.iter().any(|bid| !bid.funds_released)); if let Some(mut user_bids) = maybe_user_bids { - let mut bid = user_bids + let bid = user_bids .iter_mut() .find(|bid| !bid.funds_released) .expect("user_bids can only exist if an item here is found; qed"); @@ -581,7 +581,7 @@ fn issuer_funding_payout_one_contribution(project_id: T::ProjectIdent .find(|contributions| contributions.iter().any(|contribution| !contribution.funds_released)); if let Some(mut user_contributions) = maybe_user_contributions { - let mut contribution = user_contributions + let contribution = user_contributions .iter_mut() .find(|contribution| !contribution.funds_released) .expect("user_contributions can only exist if an item here is found; qed"); diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 91a13d9b0..809e57f9a 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -384,6 +384,10 @@ pub mod pallet { type EvaluationSuccessThreshold: Get; type Vesting: polimec_traits::ReleaseSchedule, BondTypeOf>; + /// For now we expect 3 days until the project is automatically accepted. Timeline decided by MiCA regulations. + type ManualAcceptanceDuration: Get; + /// For now we expect 4 days from acceptance to settlement due to MiCA regulations. + type SuccessToSettlementTime: Get; } #[pallet::storage] @@ -919,6 +923,10 @@ pub mod pallet { UpdateType::ProjectDecision(decision) => { unwrap_result_or_skip!(Self::do_project_decision(project_id, decision), project_id) }, + + UpdateType::StartSettlement(finalizer) => { + unwrap_result_or_skip!(Self::do_start_settlement(project_id, finalizer), project_id) + }, } } // TODO: PLMC-127. Set a proper weight diff --git a/polimec-skeleton/pallets/funding/src/mock.rs b/polimec-skeleton/pallets/funding/src/mock.rs index 542bf5e28..a68986f58 100644 --- a/polimec-skeleton/pallets/funding/src/mock.rs +++ b/polimec-skeleton/pallets/funding/src/mock.rs @@ -211,6 +211,8 @@ parameter_types! { pub const CommunityRoundDuration: BlockNumber = (5 * HOURS) as BlockNumber; pub const RemainderFundingDuration: BlockNumber = (1 * HOURS) as BlockNumber; pub const FundingPalletId: PalletId = PalletId(*b"py/cfund"); + pub const ManualAcceptanceDuration: BlockNumber = (3 * HOURS) as BlockNumber; + pub const SuccessToSettlementTime: BlockNumber =(4 * HOURS) as BlockNumber; pub PriceMap: BTreeMap = BTreeMap::from_iter(vec![ (0u32, FixedU128::from_float(69f64)), // DOT (420u32, FixedU128::from_float(0.97f64)), // USDC @@ -260,6 +262,7 @@ impl pallet_funding::Config for TestRuntime { type FeeBrackets = FeeBrackets; type FundingCurrency = StatemintAssets; type HandleMembers = Credentials; + type ManualAcceptanceDuration = ManualAcceptanceDuration; // Low value to simplify the tests type MaxBidsPerUser = ConstU32<4>; type MaxContributionsPerUser = ConstU32<4>; @@ -277,6 +280,7 @@ impl pallet_funding::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type StorageItemId = u128; type StringLimit = ConstU32<64>; + type SuccessToSettlementTime = SuccessToSettlementTime; type Vesting = Vesting; type WeightInfo = (); } diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 8e4843ca5..270c9c1cc 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -17,9 +17,6 @@ // If you feel like getting in touch with us, you can do so at info@polimec.org //! Tests for Funding pallet. - -#![feature(assert_matches)] - use super::*; use crate as pallet_funding; use crate::{ @@ -3583,8 +3580,9 @@ mod funding_end { .unwrap(); test_env.advance_time(1u64).unwrap(); - assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingSuccessful); + test_env.advance_time(::SuccessToSettlementTime::get()).unwrap(); + assert_matches!( finished_project.get_project_details().cleanup, ProjectCleanup::Ready(ProjectFinalizer::Success(_)), @@ -3620,6 +3618,7 @@ mod funding_end { test_env.advance_time(1u64).unwrap(); assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); + test_env.advance_time(::SuccessToSettlementTime::get()).unwrap(); assert_matches!( finished_project.get_project_details().cleanup, ProjectCleanup::Ready(ProjectFinalizer::Failure(_)) @@ -3627,9 +3626,39 @@ mod funding_end { test_ct_not_created_for(&test_env, project_id); - test_env.advance_time(1u64).unwrap(); + test_env.advance_time(10u64).unwrap(); assert_matches!(finished_project.get_project_details().cleanup, ProjectCleanup::Finished); } + + #[test] + fn automatic_acceptance_on_manual_decision_after_time_delta() { + let test_env = TestEnvironment::new(); + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let twenty_percent_funding_usd = Perquintill::from_percent(55) * + (project_metadata.minimum_price.checked_mul_int(project_metadata.total_allocation_size).unwrap()); + let evaluations = default_evaluations(); + let bids = generate_bids_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let contributions = + generate_contributions_from_total_usd(Percent::from_percent(50u8) * twenty_percent_funding_usd, min_price); + let finished_project = + FinishedProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids, contributions, vec![]); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); + + let project_id = finished_project.project_id; + test_env.advance_time(1u64 + ::ManualAcceptanceDuration::get()).unwrap(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingSuccessful); + test_env.advance_time(::SuccessToSettlementTime::get()).unwrap(); + + assert_matches!( + finished_project.get_project_details().cleanup, + ProjectCleanup::Ready(ProjectFinalizer::Success(_)), + ); + test_ct_created_for(&test_env, project_id); + + test_env.advance_time(10u64).unwrap(); + assert_matches!(finished_project.get_project_details().cleanup, ProjectCleanup::Finished,); + } } #[cfg(test)] diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 9928bd992..824c81a62 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -141,7 +141,7 @@ pub mod storage_types { } /// Tells on_initialize what to do with the project - #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum UpdateType { EvaluationEnd, EnglishAuctionStart, @@ -150,6 +150,7 @@ pub mod storage_types { RemainderFundingStart, FundingEnd, ProjectDecision(FundingOutcomeDecision), + StartSettlement(ProjectFinalizer), } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Ord, PartialOrd)] diff --git a/polimec-skeleton/runtime/src/lib.rs b/polimec-skeleton/runtime/src/lib.rs index e0f844981..37d96bc82 100644 --- a/polimec-skeleton/runtime/src/lib.rs +++ b/polimec-skeleton/runtime/src/lib.rs @@ -543,6 +543,16 @@ pub const CONTRIBUTION_VESTING_DURATION: BlockNumber = 365; #[cfg(not(feature = "fast-gov"))] pub const CONTRIBUTION_VESTING_DURATION: BlockNumber = 365 * DAYS; +#[cfg(feature = "fast-gov")] +pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 3; +#[cfg(not(feature = "fast-gov"))] +pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 3 * DAYS; + +#[cfg(feature = "fast-gov")] +pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 4; +#[cfg(not(feature = "fast-gov"))] +pub const SUCCESS_TO_SETTLEMENT_TIME: BlockNumber = 4 * DAYS; + parameter_types! { pub const EvaluationDuration: BlockNumber = EVALUATION_DURATION; pub const AuctionInitializePeriodDuration: BlockNumber = AUCTION_INITIALIZE_PERIOD_DURATION; @@ -551,6 +561,8 @@ parameter_types! { pub const CommunityFundingDuration: BlockNumber = COMMUNITY_FUNDING_DURATION; pub const RemainderFundingDuration: BlockNumber = REMAINDER_FUNDING_DURATION; pub const ContributionVestingDuration: BlockNumber = CONTRIBUTION_VESTING_DURATION; + pub const ManualAcceptanceDuration: BlockNumber = MANUAL_ACCEPTANCE_DURATION; + pub const SuccessToSettlementTime: BlockNumber = SUCCESS_TO_SETTLEMENT_TIME; pub const FundingPalletId: PalletId = PalletId(*b"py/cfund"); pub PriceMap: BTreeMap = BTreeMap::from_iter(vec![ (0u32, FixedU128::from_rational(69, 1)), // DOT @@ -581,6 +593,7 @@ impl pallet_funding::Config for Runtime { type FeeBrackets = FeeBrackets; type FundingCurrency = StatemintAssets; type HandleMembers = Credentials; + type ManualAcceptanceDuration = ManualAcceptanceDuration; type MaxBidsPerUser = ConstU32<256>; type MaxContributionsPerUser = ConstU32<256>; type MaxEvaluationsPerUser = (); @@ -597,6 +610,7 @@ impl pallet_funding::Config for Runtime { type RuntimeEvent = RuntimeEvent; type StorageItemId = u128; type StringLimit = ConstU32<64>; + type SuccessToSettlementTime = SuccessToSettlementTime; type Vesting = Vesting; type WeightInfo = (); }