diff --git a/pallets/funding/src/functions.rs b/pallets/funding/src/functions.rs index 085dc93ce..9aaf7d708 100644 --- a/pallets/funding/src/functions.rs +++ b/pallets/funding/src/functions.rs @@ -27,10 +27,10 @@ use frame_support::{ ensure, pallet_prelude::DispatchError, traits::{ - fungible::{InspectHold, MutateHold as FungibleMutateHold}, - fungibles::{metadata::Mutate as MetadataMutate, Create, Mutate as FungiblesMutate}, - tokens::{Precision, Preservation}, - Get, Len, + fungible::MutateHold as FungibleMutateHold, + fungibles::{metadata::Mutate as MetadataMutate, Create, Inspect, Mutate as FungiblesMutate}, + tokens::{Fortitude, Precision, Preservation, Restriction}, + Get, }, }; @@ -596,10 +596,10 @@ impl Pallet { // * Update Storage * if funding_ratio <= Perquintill::from_percent(33u64) { - project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed(vec![]); + project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed; Self::make_project_funding_fail(project_id, project_details, FailureReason::TargetNotReached, 1u32.into()) } else if funding_ratio <= Perquintill::from_percent(75u64) { - project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed(vec![]); + project_details.evaluation_round_info.evaluators_outcome = EvaluatorsOutcome::Slashed; project_details.status = ProjectStatus::AwaitingProjectDecision; Self::add_to_update_store( now + T::ManualAcceptanceDuration::get() + 1u32.into(), @@ -774,7 +774,7 @@ impl Pallet { let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let now = >::block_number(); let evaluation_id = Self::next_evaluation_id(); - let mut caller_existing_evaluations: Vec<(StorageItemIdOf, EvaluationInfoOf)> = + let caller_existing_evaluations: Vec<(StorageItemIdOf, EvaluationInfoOf)> = Evaluations::::iter_prefix((project_id, evaluator.clone())).collect(); let plmc_usd_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; let early_evaluation_reward_threshold_usd = @@ -887,7 +887,7 @@ impl Pallet { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let now = >::block_number(); let bid_id = Self::next_bid_id(); - let mut existing_bids = Bids::::iter_prefix_values((project_id, bidder.clone())).collect::>(); + let existing_bids = Bids::::iter_prefix_values((project_id, bidder.clone())).collect::>(); let ticket_size = ct_usd_price.checked_mul_int(ct_amount).ok_or(Error::::BadMath)?; let funding_asset_usd_price = @@ -935,6 +935,7 @@ impl Pallet { ct_vesting_period, when: now, funds_released: false, + ct_minted: false, }; // * Update storage * @@ -1000,7 +1001,7 @@ impl Pallet { let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let now = >::block_number(); let contribution_id = Self::next_contribution_id(); - let mut existing_contributions = + let existing_contributions = Contributions::::iter_prefix_values((project_id, contributor.clone())).collect::>(); let ct_usd_price = project_details.weighted_average_price.ok_or(Error::::AuctionNotStarted)?; @@ -1063,6 +1064,7 @@ impl Pallet { plmc_vesting_period, ct_vesting_period, funds_released: false, + ct_minted: false, }; // * Update storage * @@ -1105,8 +1107,10 @@ impl Pallet { Contributions::::insert((project_id, contributor.clone(), contribution_id), new_contribution.clone()); NextContributionId::::set(contribution_id.saturating_add(One::one())); - project_details.remaining_contribution_tokens = project_details.remaining_contribution_tokens.saturating_sub(new_contribution.ct_amount); - project_details.funding_amount_reached = project_details.funding_amount_reached.saturating_add(new_contribution.usd_contribution_amount); + project_details.remaining_contribution_tokens = + project_details.remaining_contribution_tokens.saturating_sub(new_contribution.ct_amount); + project_details.funding_amount_reached = + project_details.funding_amount_reached.saturating_add(new_contribution.usd_contribution_amount); ProjectsDetails::::insert(project_id, project_details); // If no CTs remain, end the funding phase @@ -1201,58 +1205,72 @@ impl Pallet { Ok(()) } - /// Mint contribution tokens after a step in the vesting period for a successful bid. - /// - /// # Arguments - /// * bidder: The account who made bids - /// * project_id: The project the bids where made for - /// - /// # Storage access - /// - /// * `AuctionsInfo` - Check if its time to mint some tokens based on the bid vesting period, and update the bid after minting. - /// * `T::ContributionTokenCurrency` - Mint the tokens to the bidder - pub fn do_vested_contribution_token_bid_mint_for( + pub fn do_bid_ct_mint_for( releaser: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, - ) -> Result<(), DispatchError> { + bid_id: T::StorageItemId, + ) -> DispatchResult { // * Get variables * - let bids = Bids::::iter_prefix_values((project_id, bidder.clone())); - let now = >::block_number(); - for mut bid in bids { - let mut ct_vesting = bid.ct_vesting_period; - let mut mint_amount: BalanceOf = 0u32.into(); + let mut bid = Bids::::get((project_id, bidder.clone(), bid_id)).ok_or(Error::::BidNotFound)?; + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let ct_amount = bid.final_ct_amount; - // * Validity checks * - // check that it is not too early to withdraw the next amount - if ct_vesting.next_withdrawal > now { - continue - } + // * Validity checks * + ensure!(project_details.status == ProjectStatus::FundingSuccessful, Error::::NotAllowed); + ensure!(bid.ct_minted == false, Error::::NotAllowed); + ensure!(matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..)), Error::::NotAllowed); + ensure!(T::ContributionTokenCurrency::asset_exists(project_id), Error::::CannotClaimYet); - // * Calculate variables * - // Update vesting period until the next withdrawal is in the future - while let Ok(amount) = ct_vesting.calculate_next_withdrawal() { - mint_amount = mint_amount.saturating_add(amount); - if ct_vesting.next_withdrawal > now { - break - } - } - bid.ct_vesting_period = ct_vesting; + // * Calculate variables * + bid.ct_minted = true; - // * Update storage * - // TODO: Should we mint here, or should the full mint happen to the treasury and then do transfers from there? - // Mint the funds for the user - T::ContributionTokenCurrency::mint_into(bid.project_id, &bid.bidder, mint_amount)?; - Bids::::insert((project_id, bidder.clone(), bid.id), bid.clone()); + // * Update storage * + T::ContributionTokenCurrency::mint_into(project_id, &bid.bidder, ct_amount)?; + Bids::::insert((project_id, bidder.clone(), bid_id), bid.clone()); - // * Emit events * - Self::deposit_event(Event::::ContributionTokenMinted { - caller: releaser.clone(), - project_id, - contributor: bidder.clone(), - amount: mint_amount, - }) - } + // * Emit events * + Self::deposit_event(Event::::ContributionTokenMinted { + releaser, + project_id: bid.project_id, + claimer: bidder, + amount: ct_amount, + }); + + Ok(()) + } + + pub fn do_contribution_ct_mint_for( + releaser: AccountIdOf, + project_id: T::ProjectIdentifier, + contributor: AccountIdOf, + contribution_id: T::StorageItemId, + ) -> DispatchResult { + // * Get variables * + let mut contribution = Contributions::::get((project_id, contributor.clone(), contribution_id)) + .ok_or(Error::::BidNotFound)?; + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let ct_amount = contribution.ct_amount; + + // * Validity checks * + ensure!(project_details.status == ProjectStatus::FundingSuccessful, Error::::NotAllowed); + ensure!(contribution.ct_minted == false, Error::::NotAllowed); + ensure!(T::ContributionTokenCurrency::asset_exists(project_id), Error::::CannotClaimYet); + + // * Calculate variables * + contribution.ct_minted = true; + + // * Update storage * + T::ContributionTokenCurrency::mint_into(project_id, &contribution.contributor, ct_amount)?; + Contributions::::insert((project_id, contributor.clone(), contribution_id), contribution.clone()); + + // * Emit events * + Self::deposit_event(Event::::ContributionTokenMinted { + releaser, + project_id: contribution.project_id, + claimer: contributor, + amount: ct_amount, + }); Ok(()) } @@ -1328,65 +1346,6 @@ impl Pallet { Ok(()) } - /// Mint contribution tokens after a step in the vesting period for a contribution. - /// - /// # Arguments - /// * claimer: The account who made the contribution - /// * project_id: The project the contribution was made for - /// - /// # Storage access - /// * [`ProjectsDetails`] - Check that the funding period ended - /// * [`Contributions`] - Check if its time to mint some tokens based on the contributions vesting periods, and update the contribution after minting. - /// * [`T::ContributionTokenCurrency`] - Mint the tokens to the claimer - pub fn do_vested_contribution_token_purchase_mint_for( - releaser: AccountIdOf, - project_id: T::ProjectIdentifier, - claimer: AccountIdOf, - ) -> Result<(), DispatchError> { - // * Get variables * - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; - let contributions = Contributions::::iter_prefix_values((project_id, &claimer)); - let now = >::block_number(); - - // * Validity checks * - ensure!(project_details.status == ProjectStatus::FundingSuccessful, Error::::CannotClaimYet); - // TODO: PLMC-160. Check the flow of the final_price if the final price discovery during the Auction Round fails - - for mut contribution in contributions { - let mut ct_vesting = contribution.ct_vesting_period; - let mut mint_amount: BalanceOf = 0u32.into(); - - // * Validity checks * - // check that it is not too early to withdraw the next amount - if ct_vesting.next_withdrawal > now { - continue - } - - // * Calculate variables * - // Update vesting period until the next withdrawal is in the future - while let Ok(amount) = ct_vesting.calculate_next_withdrawal() { - mint_amount = mint_amount.saturating_add(amount); - if ct_vesting.next_withdrawal > now { - break - } - } - contribution.ct_vesting_period = ct_vesting; - - Contributions::::insert((project_id, contribution.contributor.clone(), contribution.id), contribution.clone()); - - // * Emit events * - Self::deposit_event(Event::ContributionTokenMinted { - caller: releaser.clone(), - project_id, - contributor: claimer.clone(), - amount: mint_amount, - }) - } - - // * Update storage * - Ok(()) - } - pub fn do_evaluation_unbond_for( releaser: AccountIdOf, project_id: T::ProjectIdentifier, @@ -1400,7 +1359,8 @@ impl Pallet { // * Validity checks * ensure!( - released_evaluation.rewarded_or_slashed == true && + (project_details.evaluation_round_info.evaluators_outcome == EvaluatorsOutcomeOf::::Unchanged || + released_evaluation.rewarded_or_slashed == true) && matches!( project_details.status, ProjectStatus::EvaluationFailed | ProjectStatus::FundingFailed | ProjectStatus::FundingSuccessful @@ -1428,7 +1388,7 @@ impl Pallet { Ok(()) } - pub fn do_evaluation_reward( + pub fn do_evaluation_reward_payout_for( caller: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, @@ -1485,6 +1445,60 @@ impl Pallet { Ok(()) } + pub fn do_evaluation_slash_for( + caller: AccountIdOf, + project_id: T::ProjectIdentifier, + evaluator: AccountIdOf, + evaluation_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let slash_percentage = T::EvaluatorSlash::get(); + let treasury_account = T::TreasuryAccount::get(); + + let mut user_evaluations = Evaluations::::iter_prefix_values((project_id, evaluator.clone())); + let mut evaluation = + user_evaluations.find(|evaluation| evaluation.id == evaluation_id).ok_or(Error::::EvaluationNotFound)?; + + // * Validity checks * + ensure!( + evaluation.rewarded_or_slashed == false && + matches!(project_details.evaluation_round_info.evaluators_outcome, EvaluatorsOutcome::Slashed), + Error::::NotAllowed + ); + + // * Calculate variables * + // We need to make sure that the current plmc bond is always >= than the slash amount. + let slashed_amount = slash_percentage * evaluation.original_plmc_bond; + + // * Update storage * + evaluation.rewarded_or_slashed = true; + + T::NativeCurrency::transfer_on_hold( + &LockType::Evaluation(project_id), + &evaluator, + &treasury_account, + slashed_amount, + Precision::Exact, + Restriction::Free, + Fortitude::Force, + )?; + + evaluation.current_plmc_bond = evaluation.current_plmc_bond.saturating_sub(slashed_amount); + Evaluations::::insert((project_id, evaluator.clone(), evaluation.id), evaluation); + + // * Emit events * + Self::deposit_event(Event::::EvaluationSlashed { + project_id, + evaluator: evaluator.clone(), + id: evaluation_id, + amount: slashed_amount, + caller, + }); + + Ok(()) + } + pub fn do_release_bid_funds_for( _caller: AccountIdOf, _project_id: T::ProjectIdentifier, @@ -1938,20 +1952,26 @@ impl Pallet { amount: BalanceOf, ) -> Result<(), DispatchError> { // Check if the user has already locked tokens in the evaluation period - let evaluation_bonded = ::NativeCurrency::balance_on_hold(&LockType::Evaluation(project_id), who); + let user_evaluations = Evaluations::::iter_prefix_values((project_id, who.clone())); - let new_amount_to_lock = amount.saturating_sub(evaluation_bonded); - let evaluation_bonded_to_change_lock = amount.saturating_sub(new_amount_to_lock); - - T::NativeCurrency::release( - &LockType::Evaluation(project_id), - who, - evaluation_bonded_to_change_lock, - Precision::Exact, - ) - .map_err(|_| Error::::ImpossibleState)?; + let mut to_convert = amount; + for mut evaluation in user_evaluations { + if to_convert == Zero::zero() { + break + } + let slash_deposit = ::EvaluatorSlash::get() * evaluation.original_plmc_bond; + let available_to_convert = evaluation.current_plmc_bond.saturating_sub(slash_deposit); + let converted = to_convert.min(available_to_convert); + evaluation.current_plmc_bond = evaluation.current_plmc_bond.saturating_sub(converted); + Evaluations::::insert((project_id, who.clone(), evaluation.id), evaluation); + T::NativeCurrency::release(&LockType::Evaluation(project_id), who, converted, Precision::Exact) + .map_err(|_| Error::::ImpossibleState)?; + T::NativeCurrency::hold(&LockType::Participation(project_id), who, converted) + .map_err(|_| Error::::ImpossibleState)?; + to_convert = to_convert.saturating_sub(converted) + } - T::NativeCurrency::hold(&LockType::Participation(project_id), who, amount) + T::NativeCurrency::hold(&LockType::Participation(project_id), who, to_convert) .map_err(|_| Error::::InsufficientBalance)?; Ok(()) diff --git a/pallets/funding/src/impls.rs b/pallets/funding/src/impls.rs index 226662cd2..ea1a8ba25 100644 --- a/pallets/funding/src/impls.rs +++ b/pallets/funding/src/impls.rs @@ -27,10 +27,14 @@ impl DoRemainingOperation for CleanerState { } fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + let evaluators_outcome = ProjectsDetails::::get(project_id) + .ok_or(Error::::ImpossibleState)? + .evaluation_round_info + .evaluators_outcome; match self { CleanerState::Initialized(PhantomData) => { *self = Self::EvaluationRewardOrSlash( - remaining_evaluators_to_reward_or_slash::(project_id), + remaining_evaluators_to_reward_or_slash::(project_id, evaluators_outcome), PhantomData, ); Ok(Weight::zero()) @@ -134,10 +138,15 @@ impl DoRemainingOperation for CleanerState { } fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + let evaluators_outcome = ProjectsDetails::::get(project_id) + .ok_or(Error::::ImpossibleState)? + .evaluation_round_info + .evaluators_outcome; + match self { CleanerState::Initialized(PhantomData::) => { *self = CleanerState::EvaluationRewardOrSlash( - remaining_evaluators_to_reward_or_slash::(project_id), + remaining_evaluators_to_reward_or_slash::(project_id, evaluators_outcome), PhantomData::, ); Ok(Weight::zero()) @@ -222,14 +231,16 @@ impl DoRemainingOperation for CleanerState { } } -enum OperationsLeft { - Some(u64), - None, -} - -fn remaining_evaluators_to_reward_or_slash(project_id: T::ProjectIdentifier) -> u64 { - Evaluations::::iter_prefix_values((project_id,)).filter(|evaluation| !evaluation.rewarded_or_slashed).count() - as u64 +fn remaining_evaluators_to_reward_or_slash( + project_id: T::ProjectIdentifier, + outcome: EvaluatorsOutcomeOf, +) -> u64 { + if outcome == EvaluatorsOutcomeOf::::Unchanged { + 0u64 + } else { + Evaluations::::iter_prefix_values((project_id,)).filter(|evaluation| !evaluation.rewarded_or_slashed).count() + as u64 + } } fn remaining_evaluations(project_id: T::ProjectIdentifier) -> u64 { @@ -245,9 +256,8 @@ fn remaining_bids(project_id: T::ProjectIdentifier) -> u64 { } fn remaining_contributions_to_release_funds(project_id: T::ProjectIdentifier) -> u64 { - Contributions::::iter_prefix_values((project_id,)) - .filter(|contribution| !contribution.funds_released) - .count() as u64 + Contributions::::iter_prefix_values((project_id,)).filter(|contribution| !contribution.funds_released).count() + as u64 } fn remaining_contributions(project_id: T::ProjectIdentifier) -> u64 { @@ -260,9 +270,9 @@ fn remaining_bids_without_plmc_vesting(_project_id: T::ProjectIdentif 0u64 } -fn remaining_bids_without_ct_minted(_project_id: T::ProjectIdentifier) -> u64 { - // TODO: currently we vest the contribution tokens. We should change this to a direct mint. - 0u64 +fn remaining_bids_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { + let project_bids = Bids::::iter_prefix_values((project_id,)); + project_bids.filter(|bid| !bid.ct_minted).count() as u64 } fn remaining_contributions_without_plmc_vesting(_project_id: T::ProjectIdentifier) -> u64 { @@ -271,9 +281,9 @@ fn remaining_contributions_without_plmc_vesting(_project_id: T::Proje 0u64 } -fn remaining_contributions_without_ct_minted(_project_id: T::ProjectIdentifier) -> u64 { - // TODO: currently we vest the contribution tokens. We should change this to a direct mint. - 0u64 +fn remaining_contributions_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { + let project_contributions = Contributions::::iter_prefix_values((project_id,)); + project_contributions.filter(|contribution| !contribution.ct_minted).count() as u64 } fn remaining_bids_without_issuer_payout(project_id: T::ProjectIdentifier) -> u64 { @@ -286,20 +296,20 @@ fn remaining_contributions_without_issuer_payout(project_id: T::Proje fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> Result<(Weight, u64), DispatchError> { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; - let mut project_evaluations = Evaluations::::iter_prefix_values((project_id,)); + let project_evaluations = Evaluations::::iter_prefix_values((project_id,)); let mut remaining_evaluations = project_evaluations.filter(|evaluation| !evaluation.rewarded_or_slashed); - if let Some(mut evaluation) = remaining_evaluations.next() { + if let Some(evaluation) = remaining_evaluations.next() { match project_details.evaluation_round_info.evaluators_outcome { EvaluatorsOutcome::Rewarded(_) => { - match Pallet::::do_evaluation_reward( + match Pallet::::do_evaluation_reward_payout_for( T::PalletId::get().into_account_truncating(), evaluation.project_id, evaluation.evaluator.clone(), evaluation.id, ) { Ok(_) => (), - Err(e) => Pallet::::deposit_event(Event::EvaluationRewardOrSlashFailed { + Err(e) => Pallet::::deposit_event(Event::EvaluationRewardFailed { project_id: evaluation.project_id, evaluator: evaluation.evaluator.clone(), id: evaluation.id, @@ -307,25 +317,40 @@ fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) - }), }; }, - _ => (), + EvaluatorsOutcome::Slashed => { + match Pallet::::do_evaluation_slash_for( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::EvaluationSlashFailed { + project_id: evaluation.project_id, + evaluator: evaluation.evaluator.clone(), + id: evaluation.id, + error: e, + }), + }; + }, + _ => { + #[cfg(debug_assertions)] + unreachable!("EvaluatorsOutcome should be either Slashed or Rewarded if this function is called") + }, } - // if the evaluation outcome failed, we still want to flag it as rewarded or slashed. Otherwise the automatic - // transition will get stuck. - evaluation.rewarded_or_slashed = true; - Evaluations::::insert((project_id, evaluation.evaluator.clone(), evaluation.id), evaluation); - - Ok((Weight::zero(), remaining_evaluations.count() as u64)) + let remaining = remaining_evaluations.count() as u64; + Ok((Weight::zero(), remaining)) } else { Ok((Weight::zero(), 0u64)) } } fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> (Weight, u64) { - let mut project_evaluations = Evaluations::::iter_prefix_values((project_id,)).collect::>(); + let project_evaluations = Evaluations::::iter_prefix_values((project_id,)).collect::>(); let evaluation_count = project_evaluations.len() as u64; - if let Some(mut evaluation) = project_evaluations.iter().find(|evaluation| evaluation.rewarded_or_slashed) { + if let Some(evaluation) = project_evaluations.iter().next() { match Pallet::::do_evaluation_unbond_for( T::PalletId::get().into_account_truncating(), evaluation.project_id, @@ -381,7 +406,7 @@ fn unbond_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) let project_bids = Bids::::iter_prefix_values((project_id,)); let mut remaining_bids = project_bids.filter(|bid| bid.funds_released); - if let Some(mut bid) = remaining_bids.next() { + if let Some(bid) = remaining_bids.next() { match Pallet::::do_bid_unbond_for( T::PalletId::get().into_account_truncating(), bid.project_id, @@ -430,7 +455,6 @@ fn release_funds_one_contribution(project_id: T::ProjectIdentifier) - // (Weight::zero(), remaining_contributions.count() as u64) // TODO: Remove this when function is implemented (Weight::zero(), 0u64) - } else { (Weight::zero(), 0u64) } @@ -439,9 +463,10 @@ fn release_funds_one_contribution(project_id: T::ProjectIdentifier) - fn unbond_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { let project_contributions = Contributions::::iter_prefix_values((project_id,)).collect::>(); - let mut remaining_contributions = project_contributions.clone().into_iter().filter(|contribution| contribution.funds_released); + let mut remaining_contributions = + project_contributions.clone().into_iter().filter(|contribution| contribution.funds_released); - if let Some(mut contribution) = remaining_contributions.next() { + if let Some(contribution) = remaining_contributions.next() { match Pallet::::do_contribution_unbond_for( T::PalletId::get().into_account_truncating(), contribution.project_id, @@ -474,14 +499,54 @@ fn start_contribution_plmc_vesting_schedule(_project_id: T::ProjectId (Weight::zero(), 0u64) } -fn mint_ct_for_one_bid(_project_id: T::ProjectIdentifier) -> (Weight, u64) { - // TODO: Change when new vesting schedule is implemented - (Weight::zero(), 0u64) +fn mint_ct_for_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_bids = Bids::::iter_prefix_values((project_id,)); + let mut remaining_bids = project_bids.filter(|bid| !bid.ct_minted); + + if let Some(bid) = remaining_bids.next() { + match Pallet::::do_bid_ct_mint_for( + T::PalletId::get().into_account_truncating(), + bid.project_id, + bid.bidder.clone(), + bid.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::CTMintFailed { + project_id: bid.project_id, + claimer: bid.bidder.clone(), + id: bid.id, + error: e, + }), + }; + (Weight::zero(), remaining_bids.count() as u64) + } else { + (Weight::zero(), 0u64) + } } -fn mint_ct_for_one_contribution(_project_id: T::ProjectIdentifier) -> (Weight, u64) { - // TODO: Change when new vesting schedule is implemented - (Weight::zero(), 0u64) +fn mint_ct_for_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_contributions = Contributions::::iter_prefix_values((project_id,)); + let mut remaining_contributions = project_contributions.filter(|contribution| !contribution.ct_minted); + + if let Some(contribution) = remaining_contributions.next() { + match Pallet::::do_contribution_ct_mint_for( + T::PalletId::get().into_account_truncating(), + contribution.project_id, + contribution.contributor.clone(), + contribution.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::CTMintFailed { + project_id: contribution.project_id, + claimer: contribution.contributor.clone(), + id: contribution.id, + error: e, + }), + }; + (Weight::zero(), remaining_contributions.count() as u64) + } else { + (Weight::zero(), 0u64) + } } fn issuer_funding_payout_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { @@ -520,8 +585,7 @@ fn issuer_funding_payout_one_bid(project_id: T::ProjectIdentifier) -> fn issuer_funding_payout_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { let project_contributions = Contributions::::iter_prefix_values((project_id,)); - let mut remaining_contributions = project_contributions - .filter(|contribution| !contribution.funds_released); + let mut remaining_contributions = project_contributions.filter(|contribution| !contribution.funds_released); if let Some(mut contribution) = remaining_contributions.next() { match Pallet::::do_payout_contribution_funds_for( @@ -546,7 +610,6 @@ fn issuer_funding_payout_one_contribution(project_id: T::ProjectIdent // (Weight::zero(), remaining_contributions.count() as u64) // TODO: remove this when function is implemented (Weight::zero(), 0u64) - } else { (Weight::zero(), 0u64) } diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index cb2dba1a2..3d52828a0 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -124,8 +124,7 @@ //! ensure!(project_details.status == pallet_funding::ProjectStatus::CommunityRound, "Project is not in the community round"); //! //! // Calculate how much funding was done already -//! let project_contributions: ::Balance = pallet_funding::Contributions::::iter_prefix_values(project_id) -//! .flatten() +//! let project_contributions: ::Balance = pallet_funding::Contributions::::iter_prefix_values((project_id,)) //! .fold( //! 0u64.into(), //! |total_tokens_bought, contribution| { @@ -225,13 +224,13 @@ pub type AssetIdOf = <::FundingCurrency as fungibles::Inspect<::AccountId>>::AssetId; pub type RewardInfoOf = RewardInfo>; -pub type EvaluatorsOutcomeOf = EvaluatorsOutcome, BalanceOf>; +pub type EvaluatorsOutcomeOf = EvaluatorsOutcome>; pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; pub type ProjectDetailsOf = ProjectDetails, BlockNumberOf, PriceOf, BalanceOf, EvaluationRoundInfoOf>; -pub type EvaluationRoundInfoOf = EvaluationRoundInfo, BalanceOf>; +pub type EvaluationRoundInfoOf = EvaluationRoundInfo>; pub type VestingOf = Vesting, BalanceOf>; pub type EvaluationInfoOf = EvaluationInfo, ProjectIdOf, AccountIdOf, BalanceOf, BlockNumberOf>; @@ -383,6 +382,10 @@ pub mod pallet { type ManualAcceptanceDuration: Get; /// For now we expect 4 days from acceptance to settlement due to MiCA regulations. type SuccessToSettlementTime: Get; + + type EvaluatorSlash: Get; + + type TreasuryAccount: Get>; } #[pallet::storage] @@ -539,14 +542,20 @@ pub mod pallet { }, /// Contribution tokens were minted to a user ContributionTokenMinted { - caller: AccountIdOf, + releaser: AccountIdOf, project_id: T::ProjectIdentifier, - contributor: AccountIdOf, + claimer: AccountIdOf, amount: BalanceOf, }, /// A transfer of tokens failed, but because it was done inside on_initialize it cannot be solved. TransferError { error: DispatchError }, - EvaluationRewardOrSlashFailed { + EvaluationRewardFailed { + project_id: ProjectIdOf, + evaluator: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + EvaluationSlashFailed { project_id: ProjectIdOf, evaluator: AccountIdOf, id: StorageItemIdOf, @@ -595,6 +604,19 @@ pub mod pallet { amount: BalanceOf, caller: AccountIdOf, }, + EvaluationSlashed { + project_id: ProjectIdOf, + evaluator: AccountIdOf, + id: StorageItemIdOf, + amount: BalanceOf, + caller: AccountIdOf, + }, + CTMintFailed { + project_id: ProjectIdOf, + claimer: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, } #[pallet::error] @@ -787,18 +809,6 @@ pub mod pallet { Self::do_evaluate(evaluator, project_id, usd_amount) } - /// Release evaluation-bonded PLMC when a project finishes its funding round. - #[pallet::weight(T::WeightInfo::evaluation_unbond_for())] - pub fn evaluation_unbond_for( - origin: OriginFor, - bond_id: T::StorageItemId, - project_id: T::ProjectIdentifier, - evaluator: AccountIdOf, - ) -> DispatchResult { - let releaser = ensure_signed(origin)?; - Self::do_evaluation_unbond_for(releaser, project_id, evaluator, bond_id) - } - /// Bid for a project in the Auction round #[pallet::weight(T::WeightInfo::bid())] pub fn bid( @@ -833,52 +843,73 @@ pub mod pallet { Self::do_contribute(contributor, project_id, amount, multiplier, asset) } - /// Unbond some plmc from a contribution, after a step in the vesting period has passed. - pub fn vested_plmc_bid_unbond_for( + /// Release evaluation-bonded PLMC when a project finishes its funding round. + #[pallet::weight(T::WeightInfo::evaluation_unbond_for())] + pub fn evaluation_unbond_for( origin: OriginFor, project_id: T::ProjectIdentifier, - bidder: AccountIdOf, + evaluator: AccountIdOf, + bond_id: T::StorageItemId, ) -> DispatchResult { - // TODO: PLMC-157. Manage the fact that the CTs may not be claimed by those entitled let releaser = ensure_signed(origin)?; + Self::do_evaluation_unbond_for(releaser, project_id, evaluator, bond_id) + } - Self::do_vested_plmc_bid_unbond_for(releaser, project_id, bidder) + #[pallet::weight(Weight::from_parts(0, 0))] + pub fn evaluation_reward_payout_for( + origin: OriginFor, + project_id: T::ProjectIdentifier, + evaluator: AccountIdOf, + bond_id: T::StorageItemId, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + Self::do_evaluation_reward_payout_for(caller, project_id, evaluator, bond_id) } - // TODO: PLMC-157. Manage the fact that the CTs may not be claimed by those entitled - /// Mint contribution tokens after a step in the vesting period for a successful bid. - pub fn vested_contribution_token_bid_mint_for( + #[pallet::weight(Weight::from_parts(0, 0))] + pub fn bid_ct_mint_for( origin: OriginFor, project_id: T::ProjectIdentifier, bidder: AccountIdOf, + bid_id: T::StorageItemId, ) -> DispatchResult { - let releaser = ensure_signed(origin)?; + let caller = ensure_signed(origin)?; + Self::do_bid_ct_mint_for(caller, project_id, bidder, bid_id) + } - Self::do_vested_contribution_token_bid_mint_for(releaser, project_id, bidder) + #[pallet::weight(Weight::from_parts(0, 0))] + pub fn contribution_ct_mint_for( + origin: OriginFor, + project_id: T::ProjectIdentifier, + contributor: AccountIdOf, + contribution_id: T::StorageItemId, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + Self::do_contribution_ct_mint_for(caller, project_id, contributor, contribution_id) } - // TODO: PLMC-157. Manage the fact that the CTs may not be claimed by those entitled /// Unbond some plmc from a contribution, after a step in the vesting period has passed. - pub fn vested_plmc_purchase_unbond_for( + pub fn vested_plmc_bid_unbond_for( origin: OriginFor, project_id: T::ProjectIdentifier, - purchaser: AccountIdOf, + bidder: AccountIdOf, ) -> DispatchResult { + // TODO: PLMC-157. Manage the fact that the CTs may not be claimed by those entitled let releaser = ensure_signed(origin)?; - Self::do_vested_plmc_purchase_unbond_for(releaser, project_id, purchaser) + Self::do_vested_plmc_bid_unbond_for(releaser, project_id, bidder) } // TODO: PLMC-157. Manage the fact that the CTs may not be claimed by those entitled - /// Mint contribution tokens after a step in the vesting period for a contribution. - pub fn vested_contribution_token_purchase_mint_for( + /// Unbond some plmc from a contribution, after a step in the vesting period has passed. + pub fn vested_plmc_purchase_unbond_for( origin: OriginFor, project_id: T::ProjectIdentifier, purchaser: AccountIdOf, ) -> DispatchResult { let releaser = ensure_signed(origin)?; - Self::do_vested_contribution_token_purchase_mint_for(releaser, project_id, purchaser) + Self::do_vested_plmc_purchase_unbond_for(releaser, project_id, purchaser) } } diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 21bc8defe..fdb476b3b 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -214,6 +214,9 @@ parameter_types! { (Percent::from_percent(6), u128::MAX), // Making it max signifies the last bracket ]; pub EarlyEvaluationThreshold: Percent = Percent::from_percent(10); + pub EvaluatorSlash: Percent = Percent::from_percent(20); + pub TreasuryAccount: AccountId = AccountId::from(69u64); + } use frame_support::traits::WithdrawReasons; @@ -248,6 +251,7 @@ impl pallet_funding::Config for TestRuntime { type EnglishAuctionDuration = EnglishAuctionDuration; type EvaluationDuration = EvaluationDuration; type EvaluationSuccessThreshold = EarlyEvaluationThreshold; + type EvaluatorSlash = EvaluatorSlash; type FeeBrackets = FeeBrackets; type FundingCurrency = StatemintAssets; type ManualAcceptanceDuration = ManualAcceptanceDuration; @@ -269,6 +273,7 @@ impl pallet_funding::Config for TestRuntime { type StorageItemId = u128; type StringLimit = ConstU32<64>; type SuccessToSettlementTime = SuccessToSettlementTime; + type TreasuryAccount = TreasuryAccount; type Vesting = Vesting; type WeightInfo = (); } diff --git a/pallets/funding/src/tests.rs b/pallets/funding/src/tests.rs index 4e3669618..9450d3ada 100644 --- a/pallets/funding/src/tests.rs +++ b/pallets/funding/src/tests.rs @@ -38,7 +38,7 @@ use frame_support::{ use helper_functions::*; use crate::traits::BondingRequirementCalculation; -use sp_arithmetic::traits::Zero; +use sp_arithmetic::{traits::Zero, Percent, Perquintill}; use sp_runtime::{DispatchError, Either}; use sp_std::marker::PhantomData; use std::{cell::RefCell, iter::zip}; @@ -829,8 +829,13 @@ impl<'a> CommunityFundingProject<'a> { let prev_funding_asset_balances = test_env.get_free_statemint_asset_balances_for(asset_id, bidders); let plmc_evaluation_deposits: UserToPLMCBalance = calculate_evaluation_plmc_spent(evaluations.clone()); let plmc_bid_deposits: UserToPLMCBalance = calculate_auction_plmc_spent(bids.clone()); + let participation_usable_evaluation_deposits = plmc_evaluation_deposits + .clone() + .into_iter() + .map(|(acc, amount)| (acc, amount.saturating_sub(::EvaluatorSlash::get() * amount))) + .collect::(); let necessary_plmc_mint = - merge_subtract_mappings_by_user(plmc_bid_deposits.clone(), vec![plmc_evaluation_deposits]); + merge_subtract_mappings_by_user(plmc_bid_deposits.clone(), vec![participation_usable_evaluation_deposits]); let total_plmc_participation_locked = plmc_bid_deposits; let plmc_existential_deposits: UserToPLMCBalance = bids.iter().map(|bid| (bid.bidder, get_ed())).collect::<_>(); let funding_asset_deposits = calculate_auction_funding_asset_spent(bids.clone()); @@ -1002,6 +1007,9 @@ impl<'a> RemainderFundingProject<'a> { let community_funding_project = CommunityFundingProject::new_with(test_env, project_metadata, issuer, evaluations.clone(), bids.clone()); + if contributions.is_empty() { + return community_funding_project.start_remainder_or_end_funding() + } let project_id = community_funding_project.get_project_id(); let ct_price = community_funding_project.get_project_details().weighted_average_price.unwrap(); let contributors = contributions.iter().map(|cont| cont.contributor).collect::>(); @@ -1179,6 +1187,18 @@ impl<'a> FinishedProject<'a> { finished_project } + + fn from_funding_reached(test_env: &'a TestEnvironment, percent: u64) -> Self { + let project_metadata = default_project(test_env.get_new_nonce()); + let min_price = project_metadata.minimum_price; + let usd_to_reach = Perquintill::from_percent(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) * usd_to_reach, min_price); + let contributions = + generate_contributions_from_total_usd(Percent::from_percent(50u8) * usd_to_reach, min_price); + FinishedProject::new_with(test_env, project_metadata, ISSUER, evaluations, bids, contributions, vec![]) + } } mod defaults { @@ -1457,7 +1477,8 @@ pub mod helper_functions { output } - // Mappings should be sorted based on their account id, ascending. + // Accounts in base_mapping will be deducted balances from the matching accounts in substract_mappings. + // Mappings in substract_mappings without a match in base_mapping have no effect, nor will they get returned pub fn merge_subtract_mappings_by_user( base_mapping: Vec<(AccountIdOf, I)>, subtract_mappings: Vec, I)>>, @@ -1481,7 +1502,8 @@ pub mod helper_functions { break }, (None, Some(_)) => { - output.extend_from_slice(&map[j..]); + // uncomment this if we want to keep unmatched mappings on the substractor + // output.extend_from_slice(&map[j..]); break }, (Some((acc_i, val_i)), Some((acc_j, val_j))) => @@ -1493,7 +1515,8 @@ pub mod helper_functions { output.push(old_output[i]); i += 1; } else { - output.push(map[j]); + // uncomment to keep unmatched maps + // output.push(map[j]); j += 1; }, } @@ -1634,9 +1657,16 @@ pub mod helper_functions { ); }); } + + pub fn slash_evaluator_balances(mut balances: UserToPLMCBalance) -> UserToPLMCBalance { + let slash_percentage = ::EvaluatorSlash::get(); + for (_acc, balance) in balances.iter_mut() { + *balance -= slash_percentage * *balance; + } + balances + } } -#[cfg(test)] mod creation_round_success { use super::*; @@ -1692,7 +1722,6 @@ mod creation_round_success { } } -#[cfg(test)] mod creation_round_failure { use super::*; @@ -1774,7 +1803,6 @@ mod creation_round_failure { } } -#[cfg(test)] mod evaluation_round_success { use super::*; use sp_arithmetic::Perquintill; @@ -1912,7 +1940,6 @@ mod evaluation_round_success { remainder_funding_project.end_funding(); test_env.advance_time(10).unwrap(); - let deets = remainder_funding_project.get_project_details(); let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, _amount)| (*evaluator, Zero::zero())).collect(); @@ -1961,11 +1988,10 @@ mod evaluation_round_success { let increased_amounts = merge_subtract_mappings_by_user(post_free_plmc, vec![prev_free_plmc]); - assert_eq!(increased_amounts, calculate_evaluation_plmc_spent(evaluations)) + assert_eq!(increased_amounts, slash_evaluator_balances(calculate_evaluation_plmc_spent(evaluations))) } } -#[cfg(test)] mod evaluation_round_failure { use super::*; @@ -2034,7 +2060,6 @@ mod evaluation_round_failure { } } -#[cfg(test)] mod auction_round_success { use super::*; @@ -2081,10 +2106,12 @@ mod auction_round_success { let bidding_project = AuctioningProject::new_with(&test_env, project, issuer, evaluations); let already_bonded_plmc = calculate_evaluation_plmc_spent(vec![(evaluator_bidder, evaluation_amount)])[0].1; + let usable_evaluation_plmc = + already_bonded_plmc - ::EvaluatorSlash::get() * already_bonded_plmc; let necessary_plmc_for_bid = calculate_auction_plmc_spent(vec![evaluator_bid])[0].1; let necessary_usdt_for_bid = calculate_auction_funding_asset_spent(vec![evaluator_bid]); - test_env.mint_plmc_to(vec![(evaluator_bidder, necessary_plmc_for_bid - already_bonded_plmc)]); + test_env.mint_plmc_to(vec![(evaluator_bidder, necessary_plmc_for_bid - usable_evaluation_plmc)]); test_env.mint_statemint_asset_to(necessary_usdt_for_bid); bidding_project.bid_for_users(vec![evaluator_bid]).unwrap(); @@ -2111,9 +2138,9 @@ mod auction_round_success { let bid_necessary_plmc = calculate_auction_plmc_spent(vec![evaluator_bid]); let bid_necessary_usdt = calculate_auction_funding_asset_spent(vec![evaluator_bid]); - let mut evaluation_bond = sum_balance_mappings(vec![fill_necessary_plmc_for_bids, bid_necessary_plmc.clone()]); - const FUNDED_DELTA_PLMC: u128 = 69 * PLMC; - evaluation_bond -= FUNDED_DELTA_PLMC; + let evaluation_bond = sum_balance_mappings(vec![fill_necessary_plmc_for_bids, bid_necessary_plmc.clone()]); + let plmc_available_for_participation = + evaluation_bond - ::EvaluatorSlash::get() * evaluation_bond; let evaluation_usd_amount = ::PriceProvider::get_price(PLMC_STATEMINT_ID) .unwrap() @@ -2123,32 +2150,20 @@ mod auction_round_success { let bidding_project = AuctioningProject::new_with(&test_env, project, issuer, evaluations); let project_id = bidding_project.get_project_id(); - test_env.mint_plmc_to(vec![(evaluator_bidder, FUNDED_DELTA_PLMC)]); + test_env.mint_plmc_to(vec![(evaluator_bidder, evaluation_bond - plmc_available_for_participation)]); test_env.mint_statemint_asset_to(fill_necessary_usdt_for_bids); test_env.mint_statemint_asset_to(bid_necessary_usdt); bidding_project.bid_for_users(bids).unwrap(); - - let evaluation_bond = test_env.in_ext(|| { - ::NativeCurrency::balance_on_hold( - &LockType::Evaluation(project_id), - &evaluator_bidder, - ) - }); - let post_fill_evaluation_bond = bid_necessary_plmc[0].1 - FUNDED_DELTA_PLMC; - assert!( - evaluation_bond < post_fill_evaluation_bond + 10_u128 || - evaluation_bond > post_fill_evaluation_bond - 10_u128 - ); - bidding_project.bid_for_users(vec![evaluator_bid]).unwrap(); - let evaluation_bond = test_env.in_ext(|| { + + let evaluation_bonded = test_env.in_ext(|| { ::NativeCurrency::balance_on_hold( &LockType::Evaluation(project_id), &evaluator_bidder, ) }); - assert_eq!(evaluation_bond, 0); + assert_eq!(evaluation_bonded, ::EvaluatorSlash::get() * evaluation_bond); } #[test] @@ -2444,9 +2459,238 @@ mod auction_round_success { test_env.in_ext(|| Bids::::iter_prefix_values((project_id, BIDDER_2)).next().unwrap()); assert_eq!(bidder_2_bid.final_ct_usd_price.checked_mul_int(US_DOLLAR).unwrap(), 17_6_666_666_666); } + + #[test] + fn ct_minted_for_bids_automatically() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions, + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_bids = + test_env.in_ext(|| Bids::::iter_prefix_values((project_id,)).collect::>()); + assert_eq!(stored_bids.len(), bids.len()); + let user_ct_amounts = generic_map_merge_reduce( + vec![stored_bids], + |bid| bid.bidder, + BalanceOf::::zero(), + |bid, acc| acc + bid.final_ct_amount, + ); + assert_eq!(user_ct_amounts.len(), bids.len()); + + for (bidder, amount) in user_ct_amounts { + let minted = + test_env.in_ext(|| ::ContributionTokenCurrency::balance(project_id, bidder)); + assert_eq!(minted, amount); + } + } + + #[test] + fn ct_minted_for_bids_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions, + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + let stored_bids = + test_env.in_ext(|| Bids::::iter_prefix_values((project_id,)).collect::>()); + + for bid in stored_bids.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::bid_ct_mint_for( + RuntimeOrigin::signed(bid.bidder), + project_id, + bid.bidder, + bid.id, + ), + Error::::CannotClaimYet + ); + }) + } + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + for bid in stored_bids.clone() { + test_env.in_ext(|| { + Pallet::::bid_ct_mint_for( + RuntimeOrigin::signed(bid.bidder), + project_id, + bid.bidder, + bid.id, + ) + .unwrap() + }); + } + + assert_eq!(stored_bids.len(), bids.len()); + let user_ct_amounts = generic_map_merge_reduce( + vec![stored_bids], + |bid| bid.bidder, + BalanceOf::::zero(), + |bid, acc| acc + bid.final_ct_amount, + ); + assert_eq!(user_ct_amounts.len(), bids.len()); + + for (bidder, amount) in user_ct_amounts { + let minted = + test_env.in_ext(|| ::ContributionTokenCurrency::balance(project_id, bidder)); + assert_eq!(minted, amount); + } + } + + #[test] + pub fn cannot_mint_ct_twice_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions, + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + let stored_bids = + test_env.in_ext(|| Bids::::iter_prefix_values((project_id,)).collect::>()); + + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + for bid in stored_bids.clone() { + test_env.in_ext(|| { + Pallet::::bid_ct_mint_for( + RuntimeOrigin::signed(bid.bidder), + project_id, + bid.bidder, + bid.id, + ) + .unwrap(); + + assert_noop!( + Pallet::::bid_ct_mint_for( + RuntimeOrigin::signed(bid.bidder), + project_id, + bid.bidder, + bid.id, + ), + Error::::NotAllowed + ); + }); + } + } + + #[test] + pub fn cannot_mint_ct_manually_after_automatic_mint() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions, + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_bids = + test_env.in_ext(|| Bids::::iter_prefix_values((project_id,)).collect::>()); + assert_eq!(stored_bids.len(), bids.len()); + let user_ct_amounts = generic_map_merge_reduce( + vec![stored_bids.clone()], + |bid| bid.bidder, + BalanceOf::::zero(), + |bid, acc| acc + bid.final_ct_amount, + ); + assert_eq!(user_ct_amounts.len(), bids.len()); + + for (bidder, amount) in user_ct_amounts { + let minted = + test_env.in_ext(|| ::ContributionTokenCurrency::balance(project_id, bidder)); + assert_eq!(minted, amount); + } + + for bid in stored_bids.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::bid_ct_mint_for( + RuntimeOrigin::signed(bid.bidder), + project_id, + bid.bidder, + bid.id, + ), + Error::::NotAllowed + ); + }) + } + } } -#[cfg(test)] mod auction_round_failure { use super::*; @@ -2693,10 +2937,10 @@ mod auction_round_failure { } } -#[cfg(test)] mod community_round_success { use super::*; use frame_support::traits::fungible::Inspect; + use std::assert_matches::assert_matches; pub const HOURS: BlockNumber = 300u64; @@ -2788,8 +3032,9 @@ mod community_round_success { .expect("The Buyer should be able to buy multiple times"); let project_id = community_funding_project.get_project_id(); - let bob_total_contributions: BalanceOf = community_funding_project - .in_ext(|| Contributions::::iter_prefix_values((project_id, BOB)).map(|c| c.funding_asset_amount).sum()); + let bob_total_contributions: BalanceOf = community_funding_project.in_ext(|| { + Contributions::::iter_prefix_values((project_id, BOB)).map(|c| c.funding_asset_amount).sum() + }); let total_contributed = calculate_contributed_funding_asset_spent(contributions.clone(), token_price) .iter() @@ -3137,11 +3382,16 @@ mod community_round_success { let ct_price = contributing_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_bid = calculate_contributed_plmc_spent(vec![contribution], ct_price)[0].1; - let necessary_usdt_for_bid = calculate_contributed_funding_asset_spent(vec![contribution], ct_price); - - test_env.mint_plmc_to(vec![(evaluator_contributor, necessary_plmc_for_bid - already_bonded_plmc)]); - test_env.mint_statemint_asset_to(necessary_usdt_for_bid); + let plmc_available_for_participating = + already_bonded_plmc - ::EvaluatorSlash::get() * already_bonded_plmc; + let necessary_plmc_for_contribution = calculate_contributed_plmc_spent(vec![contribution], ct_price)[0].1; + let necessary_usdt_for_contribution = calculate_contributed_funding_asset_spent(vec![contribution], ct_price); + + test_env.mint_plmc_to(vec![( + evaluator_contributor, + necessary_plmc_for_contribution - plmc_available_for_participating, + )]); + test_env.mint_statemint_asset_to(necessary_usdt_for_contribution); contributing_project.buy_for_retail_users(vec![contribution]).unwrap(); } @@ -3175,9 +3425,9 @@ mod community_round_success { let overflow_necessary_usdt = calculate_contributed_funding_asset_spent(vec![overflow_contribution], expected_price); - let mut evaluation_bond = sum_balance_mappings(vec![fill_necessary_plmc, overflow_necessary_plmc.clone()]); - const FUNDED_DELTA_PLMC: u128 = 69 * PLMC; - evaluation_bond -= FUNDED_DELTA_PLMC; + let evaluation_bond = sum_balance_mappings(vec![fill_necessary_plmc, overflow_necessary_plmc.clone()]); + let plmc_available_for_participating = + evaluation_bond - ::EvaluatorSlash::get() * evaluation_bond; let evaluation_usd_amount = ::PriceProvider::get_price(PLMC_STATEMINT_ID) .unwrap() @@ -3188,163 +3438,448 @@ mod community_round_success { CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); let project_id = community_funding_project.get_project_id(); - test_env.mint_plmc_to(vec![(evaluator_contributor, FUNDED_DELTA_PLMC)]); + test_env.mint_plmc_to(vec![(evaluator_contributor, evaluation_bond - plmc_available_for_participating)]); test_env.mint_statemint_asset_to(fill_necessary_usdt); test_env.mint_statemint_asset_to(overflow_necessary_usdt); community_funding_project.buy_for_retail_users(fill_contributions).unwrap(); - - let evaluation_bond = test_env.in_ext(|| { - ::NativeCurrency::balance_on_hold( - &LockType::Evaluation(project_id), - &evaluator_contributor, - ) - }); - let post_fill_evaluation_bond = overflow_necessary_plmc[0].1 - FUNDED_DELTA_PLMC; - assert!( - evaluation_bond < post_fill_evaluation_bond + 10_u128 || - evaluation_bond > post_fill_evaluation_bond - 10_u128 - ); - community_funding_project.buy_for_retail_users(vec![overflow_contribution]).unwrap(); - let evaluation_bond = test_env.in_ext(|| { + + let evaluation_bonded = test_env.in_ext(|| { ::NativeCurrency::balance_on_hold( &LockType::Evaluation(project_id), &evaluator_contributor, ) }); - assert_eq!(evaluation_bond, 0); - } -} - -#[cfg(test)] -mod community_round_failure { - // TODO: Maybe here we can test what happens if we sell all the CTs in the community round -} - -#[cfg(test)] -mod remainder_round_success { - use super::*; - - #[test] - fn remainder_round_works() { - let test_env = TestEnvironment::new(); - let _remainder_funding_project = FinishedProject::new_with( - &test_env, - default_project(test_env.get_new_nonce()), - ISSUER, - default_evaluations(), - default_bids(), - default_community_buys(), - default_remainder_buys(), - ); + assert_eq!(evaluation_bonded, ::EvaluatorSlash::get() * evaluation_bond); } #[test] - fn remainder_contributor_was_evaluator() { + fn evaluator_cannot_use_slash_reserve_for_contributing_call_fail() { let test_env = TestEnvironment::new(); let issuer = ISSUER; let project = default_project(test_env.get_new_nonce()); let mut evaluations = default_evaluations(); - let community_contributions = default_community_buys(); let evaluator_contributor = 69; let evaluation_amount = 420 * US_DOLLAR; - let remainder_contribution = - TestContribution::new(evaluator_contributor, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT); + let contribution = + TestContribution::new(evaluator_contributor, 22 * ASSET_UNIT, None, AcceptedFundingAsset::USDT); evaluations.push((evaluator_contributor, evaluation_amount)); let bids = default_bids(); - 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 = + let contributing_project = CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); + let ct_price = contributing_project.get_project_details().weighted_average_price.unwrap(); + let necessary_plmc_for_contribution = calculate_contributed_plmc_spent(vec![contribution], ct_price)[0].1; + let plmc_evaluation_amount = 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; - let necessary_usdt_for_buy = calculate_contributed_funding_asset_spent(vec![remainder_contribution], ct_price); + let plmc_available_for_participating = + plmc_evaluation_amount - ::EvaluatorSlash::get() * plmc_evaluation_amount; + assert!( + necessary_plmc_for_contribution > plmc_available_for_participating && + necessary_plmc_for_contribution < plmc_evaluation_amount + ); + // 1199_9_999_999_999 + // 49_9_999_999_999 + let necessary_usdt_for_contribution = calculate_contributed_funding_asset_spent(vec![contribution], ct_price); - 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); + test_env.mint_statemint_asset_to(necessary_usdt_for_contribution); - remainder_funding_project.buy_for_any_user(vec![remainder_contribution]).unwrap(); + assert_matches!(contributing_project.buy_for_retail_users(vec![contribution]), Err(_)); } #[test] - fn remainder_contributor_was_evaluator_vec_full() { + fn evaluator_cannot_use_slash_reserve_for_contributing_call_success() { let test_env = TestEnvironment::new(); let issuer = ISSUER; let project = default_project(test_env.get_new_nonce()); let mut evaluations = default_evaluations(); - let bids = default_bids(); let evaluator_contributor = 69; - let overflow_contribution = - TestContribution::new(evaluator_contributor, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT); - - let mut fill_contributions = Vec::new(); - for _i in 0..::MaxContributionsPerUser::get() { - fill_contributions.push(TestContribution::new( - evaluator_contributor, - 10 * ASSET_UNIT, - None, - AcceptedFundingAsset::USDT, - )); - } - - let expected_price = calculate_price_from_test_bids(bids.clone()); - let fill_necessary_plmc = calculate_contributed_plmc_spent(fill_contributions.clone(), expected_price); - let fill_necessary_usdt_for_bids = - calculate_contributed_funding_asset_spent(fill_contributions.clone(), expected_price); - - let overflow_necessary_plmc = calculate_contributed_plmc_spent(vec![overflow_contribution], expected_price); - let overflow_necessary_usdt = - calculate_contributed_funding_asset_spent(vec![overflow_contribution], expected_price); - - let mut evaluation_bond = sum_balance_mappings(vec![fill_necessary_plmc, overflow_necessary_plmc.clone()]); - const FUNDED_DELTA_PLMC: u128 = 69 * PLMC; - evaluation_bond -= FUNDED_DELTA_PLMC; - - let evaluation_usd_amount = ::PriceProvider::get_price(PLMC_STATEMINT_ID) - .unwrap() - .saturating_mul_int(evaluation_bond); - evaluations.push((evaluator_contributor, evaluation_usd_amount)); - - let remainder_funding_project = - 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)]); - test_env.mint_statemint_asset_to(fill_necessary_usdt_for_bids); - test_env.mint_statemint_asset_to(overflow_necessary_usdt); + let evaluation_amount = 420 * US_DOLLAR; + let contribution = + TestContribution::new(evaluator_contributor, 22 * ASSET_UNIT, None, AcceptedFundingAsset::USDT); + evaluations.push((evaluator_contributor, evaluation_amount)); + let bids = default_bids(); - remainder_funding_project.buy_for_any_user(fill_contributions).unwrap(); + let contributing_project = CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); + let project_id = contributing_project.get_project_id(); - let evaluation_bond = test_env.in_ext(|| { - ::NativeCurrency::balance_on_hold( - &LockType::Evaluation(project_id), - &evaluator_contributor, - ) - }); - let post_fill_evaluation_bond = overflow_necessary_plmc[0].1 - FUNDED_DELTA_PLMC; + let ct_price = contributing_project.get_project_details().weighted_average_price.unwrap(); + let necessary_plmc_for_contribution = calculate_contributed_plmc_spent(vec![contribution], ct_price)[0].1; + let plmc_evaluation_amount = + calculate_evaluation_plmc_spent(vec![(evaluator_contributor, evaluation_amount)])[0].1; + let plmc_available_for_participating = + plmc_evaluation_amount - ::EvaluatorSlash::get() * plmc_evaluation_amount; assert!( - evaluation_bond < post_fill_evaluation_bond + 10_u128 || - evaluation_bond > post_fill_evaluation_bond - 10_u128 + necessary_plmc_for_contribution > plmc_available_for_participating && + necessary_plmc_for_contribution < plmc_evaluation_amount ); + let necessary_usdt_for_contribution = calculate_contributed_funding_asset_spent(vec![contribution], ct_price); - remainder_funding_project.buy_for_any_user(vec![overflow_contribution]).unwrap(); - let evaluation_bond = test_env.in_ext(|| { - ::NativeCurrency::balance_on_hold( - &LockType::Evaluation(project_id), - &evaluator_contributor, - ) - }); - assert_eq!(evaluation_bond, 0); + test_env.mint_plmc_to(vec![( + evaluator_contributor, + necessary_plmc_for_contribution - plmc_available_for_participating, + )]); + test_env.mint_statemint_asset_to(necessary_usdt_for_contribution); + + contributing_project.buy_for_retail_users(vec![contribution]).unwrap(); + let evaluation_locked = + test_env.get_reserved_plmc_balances_for(vec![evaluator_contributor], LockType::Evaluation(project_id))[0].1; + let participation_locked = test_env + .get_reserved_plmc_balances_for(vec![evaluator_contributor], LockType::Participation(project_id))[0] + .1; + + assert_eq!(evaluation_locked, ::EvaluatorSlash::get() * plmc_evaluation_amount); + assert_eq!(participation_locked, necessary_plmc_for_contribution); } #[test] - fn remainder_round_ends_on_all_ct_sold_exact() { + fn ct_minted_for_community_buys_automatically() { let test_env = TestEnvironment::new(); - let remainder_funding_project = RemainderFundingProject::new_with( + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions.clone(), + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_community_buys = + test_env.in_ext(|| Contributions::::iter_prefix_values((project_id,)).collect::>()); + assert_eq!(stored_community_buys.len(), community_contributions.len()); + let user_ct_amounts = generic_map_merge_reduce( + vec![stored_community_buys], + |contribution| contribution.contributor, + BalanceOf::::zero(), + |contribution, acc| acc + contribution.ct_amount, + ); + assert_eq!(user_ct_amounts.len(), community_contributions.len()); + + for (contributor, amount) in user_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + } + + #[test] + fn ct_minted_for_community_buys_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions.clone(), + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + let stored_contributions = + test_env.in_ext(|| Contributions::::iter_prefix_values((project_id,)).collect::>()); + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::CannotClaimYet + ); + }) + } + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ) + .unwrap() + }); + } + + assert_eq!(stored_contributions.len(), community_contributions.len()); + let user_ct_amounts = generic_map_merge_reduce( + vec![stored_contributions], + |contribution| contribution.contributor, + BalanceOf::::zero(), + |contribution, acc| acc + contribution.ct_amount, + ); + assert_eq!(user_ct_amounts.len(), community_contributions.len()); + + for (contributor, amount) in user_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + } + + #[test] + pub fn cannot_mint_ct_twice_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions.clone(), + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + let stored_contributions = + test_env.in_ext(|| Contributions::::iter_prefix_values((project_id,)).collect::>()); + + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ) + .unwrap(); + + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::NotAllowed + ); + }); + } + } + + #[test] + pub fn cannot_mint_ct_manually_after_automatic_mint() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions.clone(), + remainder_contributions, + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_contributions = + test_env.in_ext(|| Contributions::::iter_prefix_values((project_id,)).collect::>()); + assert_eq!(stored_contributions.len(), community_contributions.len()); + let user_ct_amounts = generic_map_merge_reduce( + vec![stored_contributions.clone()], + |contribution| contribution.contributor, + BalanceOf::::zero(), + |contribution, acc| acc + contribution.ct_amount, + ); + assert_eq!(user_ct_amounts.len(), community_contributions.len()); + + for (contributor, amount) in user_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::NotAllowed + ); + }) + } + } +} + +mod community_round_failure { + // TODO: Maybe here we can test what happens if we sell all the CTs in the community round +} + +mod remainder_round_success { + use super::*; + use crate::tests::testing_macros::extract_from_event; + + #[test] + fn remainder_round_works() { + let test_env = TestEnvironment::new(); + let _remainder_funding_project = FinishedProject::new_with( + &test_env, + default_project(test_env.get_new_nonce()), + ISSUER, + default_evaluations(), + default_bids(), + default_community_buys(), + default_remainder_buys(), + ); + } + + #[test] + fn remainder_contributor_was_evaluator() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let mut evaluations = default_evaluations(); + let community_contributions = default_community_buys(); + let evaluator_contributor = 69; + let evaluation_amount = 420 * US_DOLLAR; + let remainder_contribution = + TestContribution::new(evaluator_contributor, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT); + evaluations.push((evaluator_contributor, evaluation_amount)); + let bids = default_bids(); + + 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 plmc_available_for_contribution = + already_bonded_plmc - ::EvaluatorSlash::get() * already_bonded_plmc; + let necessary_plmc_for_buy = calculate_contributed_plmc_spent(vec![remainder_contribution], ct_price)[0].1; + let necessary_usdt_for_buy = calculate_contributed_funding_asset_spent(vec![remainder_contribution], ct_price); + + test_env.mint_plmc_to(vec![(evaluator_contributor, necessary_plmc_for_buy - plmc_available_for_contribution)]); + test_env.mint_statemint_asset_to(necessary_usdt_for_buy); + + remainder_funding_project.buy_for_any_user(vec![remainder_contribution]).unwrap(); + } + + #[test] + fn remainder_contributor_was_evaluator_vec_full() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let mut evaluations = default_evaluations(); + let bids = default_bids(); + let evaluator_contributor = 69; + let overflow_contribution = + TestContribution::new(evaluator_contributor, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT); + + let mut fill_contributions = Vec::new(); + for _i in 0..::MaxContributionsPerUser::get() { + fill_contributions.push(TestContribution::new( + evaluator_contributor, + 10 * ASSET_UNIT, + None, + AcceptedFundingAsset::USDT, + )); + } + + let expected_price = calculate_price_from_test_bids(bids.clone()); + let fill_necessary_plmc = calculate_contributed_plmc_spent(fill_contributions.clone(), expected_price); + let fill_necessary_usdt_for_bids = + calculate_contributed_funding_asset_spent(fill_contributions.clone(), expected_price); + + let overflow_necessary_plmc = calculate_contributed_plmc_spent(vec![overflow_contribution], expected_price); + let overflow_necessary_usdt = + calculate_contributed_funding_asset_spent(vec![overflow_contribution], expected_price); + + let evaluation_bond = sum_balance_mappings(vec![fill_necessary_plmc, overflow_necessary_plmc.clone()]); + let plmc_available_for_participating = + evaluation_bond - ::EvaluatorSlash::get() * evaluation_bond; + + let evaluation_usd_amount = ::PriceProvider::get_price(PLMC_STATEMINT_ID) + .unwrap() + .saturating_mul_int(evaluation_bond); + evaluations.push((evaluator_contributor, evaluation_usd_amount)); + + let remainder_funding_project = + 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, evaluation_bond - plmc_available_for_participating)]); + test_env.mint_statemint_asset_to(fill_necessary_usdt_for_bids); + test_env.mint_statemint_asset_to(overflow_necessary_usdt); + + remainder_funding_project.buy_for_any_user(fill_contributions).unwrap(); + remainder_funding_project.buy_for_any_user(vec![overflow_contribution]).unwrap(); + + let evaluation_bonded = test_env.in_ext(|| { + ::NativeCurrency::balance_on_hold( + &LockType::Evaluation(project_id), + &evaluator_contributor, + ) + }); + assert_eq!(evaluation_bonded, ::EvaluatorSlash::get() * evaluation_bond); + } + + #[test] + fn remainder_round_ends_on_all_ct_sold_exact() { + let test_env = TestEnvironment::new(); + let remainder_funding_project = RemainderFundingProject::new_with( &test_env, default_project(0), ISSUER, @@ -3464,13 +3999,359 @@ mod remainder_round_success { remainder_funding_project.get_project_id(), ); } + + #[test] + fn ct_minted_for_remainder_buys_automatically() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = + vec![(EVALUATOR_1, 50_000 * PLMC), (EVALUATOR_2, 25_000 * PLMC), (EVALUATOR_3, 32_000 * PLMC)]; + let bids = vec![ + TestBid::new(BIDDER_1, 50000 * ASSET_UNIT, 18_u128.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 40000 * ASSET_UNIT, 15_u128.into(), None, AcceptedFundingAsset::USDT), + ]; + let community_contributions = vec![ + TestContribution::new(BUYER_1, 100 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 200 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + let remainder_contributions = vec![ + TestContribution::new(EVALUATOR_2, 300 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BIDDER_1, 4000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions.clone(), + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let evaluator_2_reward = extract_from_event!( + &test_env, + Event::::EvaluationRewarded { evaluator: EVALUATOR_2, amount, .. }, + amount + ) + .unwrap(); + + let total_remainder_participant_ct_amounts = vec![ + (EVALUATOR_2, 300 * ASSET_UNIT + evaluator_2_reward), + (BUYER_2, 600 * ASSET_UNIT + 200 * ASSET_UNIT), + (BIDDER_1, 50000 * ASSET_UNIT + 4000 * ASSET_UNIT), + ]; + for (contributor, amount) in total_remainder_participant_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + } + + #[test] + fn ct_minted_for_community_buys_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = + vec![(EVALUATOR_1, 50_000 * PLMC), (EVALUATOR_2, 25_000 * PLMC), (EVALUATOR_3, 32_000 * PLMC)]; + let bids = vec![ + TestBid::new(BIDDER_1, 50000 * ASSET_UNIT, 18_u128.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 40000 * ASSET_UNIT, 15_u128.into(), None, AcceptedFundingAsset::USDT), + ]; + let community_contributions = vec![ + TestContribution::new(BUYER_1, 100 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 200 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + let remainder_contributions = vec![ + TestContribution::new(EVALUATOR_2, 300 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BIDDER_1, 4000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions.clone(), + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + let stored_contributions = test_env.in_ext(|| { + let evaluator_contribution = + Contributions::::iter_prefix_values((project_id, EVALUATOR_2)).next().unwrap(); + let buyer_contribution = + Contributions::::iter_prefix_values((project_id, BUYER_2)).next().unwrap(); + let bidder_contribution = + Contributions::::iter_prefix_values((project_id, BIDDER_1)).next().unwrap(); + vec![evaluator_contribution.clone(), buyer_contribution.clone(), bidder_contribution.clone()] + }); + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::CannotClaimYet + ); + }) + } + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ) + .unwrap() + }); + } + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let evaluator_2_reward = extract_from_event!( + &test_env, + Event::::EvaluationRewarded { evaluator: EVALUATOR_2, amount, .. }, + amount + ) + .unwrap(); + + let total_remainder_participant_ct_amounts = vec![ + (EVALUATOR_2, 300 * ASSET_UNIT + evaluator_2_reward), + (BUYER_2, 600 * ASSET_UNIT + 200 * ASSET_UNIT), + (BIDDER_1, 50000 * ASSET_UNIT + 4000 * ASSET_UNIT), + ]; + for (contributor, amount) in total_remainder_participant_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + } + + #[test] + pub fn cannot_mint_ct_twice_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = + vec![(EVALUATOR_1, 50_000 * PLMC), (EVALUATOR_2, 25_000 * PLMC), (EVALUATOR_3, 32_000 * PLMC)]; + let bids = vec![ + TestBid::new(BIDDER_1, 50000 * ASSET_UNIT, 18_u128.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 40000 * ASSET_UNIT, 15_u128.into(), None, AcceptedFundingAsset::USDT), + ]; + let community_contributions = vec![ + TestContribution::new(BUYER_1, 100 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 200 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + let remainder_contributions = vec![ + TestContribution::new(EVALUATOR_2, 300 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BIDDER_1, 4000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions.clone(), + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + let stored_contributions = test_env.in_ext(|| { + let evaluator_contribution = + Contributions::::iter_prefix_values((project_id, EVALUATOR_2)).next().unwrap(); + let buyer_contribution = + Contributions::::iter_prefix_values((project_id, BUYER_2)).next().unwrap(); + let bidder_contribution = + Contributions::::iter_prefix_values((project_id, BIDDER_1)).next().unwrap(); + vec![evaluator_contribution.clone(), buyer_contribution.clone(), bidder_contribution.clone()] + }); + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::CannotClaimYet + ); + }) + } + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ) + .unwrap(); + + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::NotAllowed + ); + }); + } + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let evaluator_2_reward = extract_from_event!( + &test_env, + Event::::EvaluationRewarded { evaluator: EVALUATOR_2, amount, .. }, + amount + ) + .unwrap(); + + let total_remainder_participant_ct_amounts = vec![ + (EVALUATOR_2, 300 * ASSET_UNIT + evaluator_2_reward), + (BUYER_2, 600 * ASSET_UNIT + 200 * ASSET_UNIT), + (BIDDER_1, 50000 * ASSET_UNIT + 4000 * ASSET_UNIT), + ]; + for (contributor, amount) in total_remainder_participant_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + } + + #[test] + pub fn cannot_mint_ct_manually_after_automatic_mint() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = + vec![(EVALUATOR_1, 50_000 * PLMC), (EVALUATOR_2, 25_000 * PLMC), (EVALUATOR_3, 32_000 * PLMC)]; + let bids = vec![ + TestBid::new(BIDDER_1, 50000 * ASSET_UNIT, 18_u128.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 40000 * ASSET_UNIT, 15_u128.into(), None, AcceptedFundingAsset::USDT), + ]; + let community_contributions = vec![ + TestContribution::new(BUYER_1, 100 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 200 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + let remainder_contributions = vec![ + TestContribution::new(EVALUATOR_2, 300 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BIDDER_1, 4000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions.clone(), + ); + let project_id = finished_project.get_project_id(); + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + let stored_contributions = test_env.in_ext(|| { + let evaluator_contribution = + Contributions::::iter_prefix_values((project_id, EVALUATOR_2)).next().unwrap(); + let buyer_contribution = + Contributions::::iter_prefix_values((project_id, BUYER_2)).next().unwrap(); + let bidder_contribution = + Contributions::::iter_prefix_values((project_id, BIDDER_1)).next().unwrap(); + vec![evaluator_contribution.clone(), buyer_contribution.clone(), bidder_contribution.clone()] + }); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let evaluator_2_reward = extract_from_event!( + &test_env, + Event::::EvaluationRewarded { evaluator: EVALUATOR_2, amount, .. }, + amount + ) + .unwrap(); + + let total_remainder_participant_ct_amounts = vec![ + (EVALUATOR_2, 300 * ASSET_UNIT + evaluator_2_reward), + (BUYER_2, 600 * ASSET_UNIT + 200 * ASSET_UNIT), + (BIDDER_1, 50000 * ASSET_UNIT + 4000 * ASSET_UNIT), + ]; + for (contributor, amount) in total_remainder_participant_ct_amounts { + let minted = test_env + .in_ext(|| ::ContributionTokenCurrency::balance(project_id, contributor)); + assert_eq!(minted, amount); + } + + for contribution in stored_contributions.clone() { + test_env.in_ext(|| { + assert_noop!( + Pallet::::contribution_ct_mint_for( + RuntimeOrigin::signed(contribution.contributor), + project_id, + contribution.contributor, + contribution.id, + ), + Error::::NotAllowed + ); + }); + } + } } -#[cfg(test)] mod funding_end { use super::*; use sp_arithmetic::{Percent, Perquintill}; use std::assert_matches::assert_matches; + use testing_macros::call_and_is_ok; #[test] fn automatic_fail_less_eq_33_percent() { @@ -3667,201 +4548,139 @@ mod funding_end { Cleaner::Success(CleanerState::Finished(PhantomData)) ); } -} - -#[cfg(test)] -mod purchased_vesting { - use super::*; #[test] - fn individual_contribution_token_mints() { - // TODO: currently the vesting is limited to the whole payment at once. We should test it with several payments over a vesting period. + fn evaluators_get_slashed_funding_accepted() { let test_env = TestEnvironment::new(); - let community_contributions = default_community_buys(); - let remainder_contributions = default_remainder_buys(); - let finished_project = FinishedProject::new_with( - &test_env, - default_project(test_env.get_new_nonce()), - ISSUER, - default_evaluations(), - default_bids(), - community_contributions.clone(), - remainder_contributions.clone(), + let finished_project = FinishedProject::from_funding_reached(&test_env, 43u64); + let project_id = finished_project.get_project_id(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); + + let old_evaluation_locked_plmc: UserToPLMCBalance = test_env + .get_all_reserved_plmc_balances(LockType::Evaluation(project_id)) + .into_iter() + .filter(|(_acc, amount)| amount > &Zero::zero()) + .collect::>(); + + let evaluators = old_evaluation_locked_plmc.iter().map(|(acc, _)| acc.clone()).collect::>(); + + let old_participation_locked_plmc = + test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Participation(project_id)); + let old_free_plmc: UserToPLMCBalance = test_env.get_free_plmc_balances_for(evaluators.clone()); + + call_and_is_ok!( + test_env, + FundingModule::do_decide_project_outcome( + ISSUER, + finished_project.project_id, + FundingOutcomeDecision::AcceptFunding + ) ); - let project_id = finished_project.project_id; - let user_buys = generic_map_merge( - vec![community_contributions.clone(), default_remainder_buys()], - |m| m.contributor.clone(), - |m1, m2| { - let total_amount = m1.amount.clone() + m2.amount.clone(); - let mut mx = m1.clone(); - mx.amount = total_amount; - mx - }, + test_env.advance_time(1u64).unwrap(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingSuccessful); + test_env.advance_time(::SuccessToSettlementTime::get() + 10u64).unwrap(); + assert_matches!( + finished_project.get_project_details().cleanup, + Cleaner::Success(CleanerState::Finished(PhantomData)) ); - for merged_contribution in user_buys { - let result = test_env.in_ext(|| { - FundingModule::vested_contribution_token_purchase_mint_for( - RuntimeOrigin::signed(merged_contribution.contributor), - project_id, - merged_contribution.contributor, - ) - }); - assert_ok!(result); - let minted_balance = test_env.in_ext(|| { - ::ContributionTokenCurrency::balance(project_id, merged_contribution.contributor) - }); - let desired_balance = merged_contribution.amount; - assert_eq!(minted_balance, desired_balance); - } + let slashed_evaluation_locked_plmc = slash_evaluator_balances(old_evaluation_locked_plmc); + let expected_evaluator_free_balances = merge_add_mappings_by_user(vec![ + slashed_evaluation_locked_plmc, + old_participation_locked_plmc, + old_free_plmc, + ]); + + let actual_evaluator_free_balances = test_env.get_free_plmc_balances_for(evaluators.clone()); + + assert_eq!(actual_evaluator_free_balances, expected_evaluator_free_balances); } #[test] - fn plmc_unbonded() { + fn evaluators_get_slashed_funding_funding_rejected() { let test_env = TestEnvironment::new(); - let evaluations = default_evaluations(); - let bids = default_bids(); - let community_contributions = default_community_buys(); - let remainder_contributions = default_remainder_buys(); - let finished_project = FinishedProject::new_with( - &test_env, - default_project(test_env.get_new_nonce()), - ISSUER, - evaluations.clone(), - bids.clone(), - community_contributions.clone(), - remainder_contributions.clone(), + let finished_project = FinishedProject::from_funding_reached(&test_env, 56u64); + let project_id = finished_project.get_project_id(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::AwaitingProjectDecision); + + let old_evaluation_locked_plmc: UserToPLMCBalance = test_env + .get_all_reserved_plmc_balances(LockType::Evaluation(project_id)) + .into_iter() + .filter(|(_acc, amount)| amount > &Zero::zero()) + .collect::>(); + + let evaluators = old_evaluation_locked_plmc.iter().map(|(acc, _)| acc.clone()).collect::>(); + + let old_participation_locked_plmc = + test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Participation(project_id)); + let old_free_plmc: UserToPLMCBalance = test_env.get_free_plmc_balances_for(evaluators.clone()); + + call_and_is_ok!( + test_env, + FundingModule::do_decide_project_outcome( + ISSUER, + finished_project.project_id, + FundingOutcomeDecision::RejectFunding + ) ); - let project_id = finished_project.project_id; - let token_price = finished_project.get_project_details().weighted_average_price.unwrap(); - - let bidders_plmc_bond = calculate_auction_plmc_spent(bids.clone()); - let contributors_plmc_spent: UserToPLMCBalance = generic_map_merge_reduce( - vec![community_contributions.clone(), remainder_contributions.clone()], - |m| m.contributor.clone(), - 0_u128, - |contribution, total_plmc_spent| { - let new_plmc = calculate_contributed_plmc_spent(vec![contribution.clone()], token_price)[0].1; - total_plmc_spent.checked_add(new_plmc).unwrap() - }, + test_env.advance_time(1u64).unwrap(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); + test_env.advance_time(::SuccessToSettlementTime::get() + 10u64).unwrap(); + assert_matches!( + finished_project.get_project_details().cleanup, + Cleaner::Failure(CleanerState::Finished(PhantomData)) ); - let participation_locked_plmc = - merge_add_mappings_by_user(vec![bidders_plmc_bond.clone(), contributors_plmc_spent.clone()]); - let purchase_unbonds = - merge_subtract_mappings_by_user(participation_locked_plmc.clone(), vec![bidders_plmc_bond.clone()]); - - for ((user, pre_locked), (_, post_released)) in zip(participation_locked_plmc, purchase_unbonds) { - let actual_bonded_plmc = test_env.in_ext(|| { - ::NativeCurrency::balance_on_hold(&LockType::Participation(project_id), &user) - }); + let slashed_evaluation_locked_plmc = slash_evaluator_balances(old_evaluation_locked_plmc); + let expected_evaluator_free_balances = merge_add_mappings_by_user(vec![ + slashed_evaluation_locked_plmc, + old_participation_locked_plmc, + old_free_plmc, + ]); - assert_eq!(actual_bonded_plmc, pre_locked); + let actual_evaluator_free_balances = test_env.get_free_plmc_balances_for(evaluators.clone()); - let result = test_env.in_ext(|| { - FundingModule::vested_plmc_purchase_unbond_for(RuntimeOrigin::signed(user), project_id, user) - }); - let actual_bonded_plmc = test_env.in_ext(|| { - ::NativeCurrency::balance_on_hold(&LockType::Participation(project_id), &user) - }); - assert_ok!(result); - assert_eq!(actual_bonded_plmc, pre_locked - post_released); - } + assert_eq!(actual_evaluator_free_balances, expected_evaluator_free_balances); } -} - -#[cfg(test)] -mod bids_vesting { - use super::*; #[test] - fn contribution_token_mints() { + fn evaluators_get_slashed_funding_failed() { let test_env = TestEnvironment::new(); - let bids = default_bids(); - let finished_project = FinishedProject::new_with( - &test_env, - default_project(test_env.get_new_nonce()), - ISSUER, - default_evaluations(), - bids.clone(), - default_community_buys(), - default_remainder_buys(), - ); - let project_id = finished_project.project_id; + let finished_project = FinishedProject::from_funding_reached(&test_env, 24u64); + let project_id = finished_project.get_project_id(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); - for bid in bids { - let actual_ct_balance = - test_env.in_ext(|| ::ContributionTokenCurrency::balance(project_id, bid.bidder)); - assert_eq!(actual_ct_balance, 0u32.into()); + let old_evaluation_locked_plmc: UserToPLMCBalance = test_env + .get_all_reserved_plmc_balances(LockType::Evaluation(project_id)) + .into_iter() + .filter(|(_acc, amount)| amount > &Zero::zero()) + .collect::>(); - let result = test_env.in_ext(|| { - FundingModule::vested_contribution_token_bid_mint_for( - RuntimeOrigin::signed(bid.bidder), - project_id, - bid.bidder, - ) - }); - assert_ok!(result); - let minted_balance = - test_env.in_ext(|| ::ContributionTokenCurrency::balance(project_id, bid.bidder)); - assert_eq!(minted_balance, bid.amount); - } - } + let evaluators = old_evaluation_locked_plmc.iter().map(|(acc, _)| acc.clone()).collect::>(); - #[test] - fn plmc_unbonded() { - let test_env = TestEnvironment::new(); - let bids = default_bids(); - let community_contributions = default_community_buys(); - let remainder_contributions = default_remainder_buys(); - let finished_project = FinishedProject::new_with( - &test_env, - default_project(test_env.get_new_nonce()), - ISSUER, - default_evaluations(), - bids.clone(), - default_community_buys(), - default_remainder_buys(), + let old_participation_locked_plmc = + test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Participation(project_id)); + let old_free_plmc: UserToPLMCBalance = test_env.get_free_plmc_balances_for(evaluators.clone()); + + test_env.advance_time(::SuccessToSettlementTime::get() + 10u64).unwrap(); + assert_matches!( + finished_project.get_project_details().cleanup, + Cleaner::Failure(CleanerState::Finished(PhantomData)) ); - let project_id = finished_project.project_id; - let ct_price = finished_project.get_project_details().weighted_average_price.unwrap(); - let plmc_bid_deposits = calculate_auction_plmc_spent_after_price_calculation(bids.clone(), ct_price); - let plmc_community_contribution_deposits = - calculate_contributed_plmc_spent(community_contributions.clone(), ct_price); - let plmc_remainder_contribution_deposits = - calculate_contributed_plmc_spent(remainder_contributions.clone(), ct_price); - let total_plmc_participation_locked = merge_add_mappings_by_user(vec![ - plmc_bid_deposits.clone(), - plmc_community_contribution_deposits, - plmc_remainder_contribution_deposits.clone(), + let slashed_evaluation_locked_plmc = slash_evaluator_balances(old_evaluation_locked_plmc); + let expected_evaluator_free_balances = merge_add_mappings_by_user(vec![ + slashed_evaluation_locked_plmc, + old_participation_locked_plmc, + old_free_plmc, ]); - test_env - .do_reserved_plmc_assertions(total_plmc_participation_locked.clone(), LockType::Participation(project_id)); - - for (bidder, deposit) in plmc_bid_deposits { - let bidder_participation_locked = total_plmc_participation_locked - .clone() - .into_iter() - .find(|(acc, _)| acc.clone() == bidder.clone()) - .unwrap() - .1; - let result = test_env.in_ext(|| { - FundingModule::vested_plmc_bid_unbond_for(RuntimeOrigin::signed(bidder.clone()), project_id, bidder) - }); - assert_ok!(result); + let actual_evaluator_free_balances = test_env.get_free_plmc_balances_for(evaluators.clone()); - test_env.do_reserved_plmc_assertions( - vec![(bidder, bidder_participation_locked - deposit)], - LockType::Participation(project_id), - ); - } + assert_eq!(actual_evaluator_free_balances, expected_evaluator_free_balances); } } -#[cfg(test)] mod test_helper_functions { use super::*; @@ -4112,7 +4931,6 @@ mod test_helper_functions { } } -#[cfg(test)] mod misc_features { use super::*; use crate::UpdateType::{CommunityFundingStart, RemainderFundingStart}; @@ -4138,14 +4956,13 @@ mod misc_features { }); } - #[allow(dead_code)] + #[test] fn sandbox() { - assert!(true) + assert!(true); } } mod testing_macros { - #[allow(unused_macros)] macro_rules! assert_close_enough { ($real:expr, $desired:expr, $max_approximation:expr) => { let real_parts = Perquintill::from_rational($real, $desired); @@ -4155,4 +4972,55 @@ mod testing_macros { }; } pub(crate) use assert_close_enough; + + macro_rules! call_and_is_ok { + ($env: expr, $call: expr) => { + $env.ext_env.borrow_mut().execute_with(|| { + let result = $call; + assert!(result.is_ok(), "Call failed: {:?}", result); + }); + }; + } + pub(crate) use call_and_is_ok; + + #[allow(unused_macros)] + macro_rules! find_event { + ($env: expr, $pattern:pat) => { + $env.ext_env.borrow_mut().execute_with(|| { + let events = System::events(); + + events.iter().find_map(|event_record| { + if let frame_system::EventRecord { + event: RuntimeEvent::FundingModule(desired_event @ $pattern), + .. + } = event_record + { + Some(desired_event.clone()) + } else { + None + } + }) + }) + }; + } + #[allow(unused_imports)] + pub(crate) use find_event; + + macro_rules! extract_from_event { + ($env: expr, $pattern:pat, $field:ident) => { + $env.ext_env.borrow_mut().execute_with(|| { + let events = System::events(); + + events.iter().find_map(|event_record| { + if let frame_system::EventRecord { event: RuntimeEvent::FundingModule($pattern), .. } = event_record + { + Some($field.clone()) + } else { + None + } + }) + }) + }; + } + pub(crate) use extract_from_event; } diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index 95fb2a0e6..6764d9c91 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -198,6 +198,7 @@ pub mod storage_types { pub ct_vesting_period: CTVesting, pub when: BlockNumber, pub funds_released: bool, + pub ct_minted: bool, } impl< @@ -250,6 +251,7 @@ pub mod storage_types { pub plmc_vesting_period: PLMCVesting, pub ct_vesting_period: CTVesting, pub funds_released: bool, + pub ct_minted: bool, } } @@ -540,17 +542,17 @@ pub mod inner_types { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct EvaluationRoundInfo { + pub struct EvaluationRoundInfo { pub total_bonded_usd: Balance, pub total_bonded_plmc: Balance, - pub evaluators_outcome: EvaluatorsOutcome, + pub evaluators_outcome: EvaluatorsOutcome, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub enum EvaluatorsOutcome { + pub enum EvaluatorsOutcome { Unchanged, Rewarded(RewardInfo), - Slashed(Vec<(AccountId, Balance)>), + Slashed, } #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index c6f8bc686..113b28859 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -4631,7 +4631,7 @@ fn test_delegator_with_deprecated_status_leaving_can_schedule_leave_delegators_a .build() .execute_with(|| { >::mutate(2, |value| { - value.as_mut().map(|mut state| { + value.as_mut().map(|state| { state.status = DelegatorStatus::Leaving(2); }) }); @@ -4659,7 +4659,7 @@ fn test_delegator_with_deprecated_status_leaving_can_cancel_leave_delegators_as_ .build() .execute_with(|| { >::mutate(2, |value| { - value.as_mut().map(|mut state| { + value.as_mut().map(|state| { state.status = DelegatorStatus::Leaving(2); }) }); @@ -4684,7 +4684,7 @@ fn test_delegator_with_deprecated_status_leaving_can_execute_leave_delegators_as .build() .execute_with(|| { >::mutate(2, |value| { - value.as_mut().map(|mut state| { + value.as_mut().map(|state| { state.status = DelegatorStatus::Leaving(2); }) }); @@ -4710,7 +4710,7 @@ fn test_delegator_with_deprecated_status_leaving_cannot_execute_leave_delegators .build() .execute_with(|| { >::mutate(2, |value| { - value.as_mut().map(|mut state| { + value.as_mut().map(|state| { state.status = DelegatorStatus::Leaving(2); }) }); @@ -4959,7 +4959,7 @@ fn test_execute_leave_delegators_with_deprecated_status_leaving_removes_auto_com )); >::mutate(2, |value| { - value.as_mut().map(|mut state| { + value.as_mut().map(|state| { state.status = DelegatorStatus::Leaving(2); }) }); diff --git a/runtimes/shared-configuration/src/funding.rs b/runtimes/shared-configuration/src/funding.rs index e0752f8d3..ac64720df 100644 --- a/runtimes/shared-configuration/src/funding.rs +++ b/runtimes/shared-configuration/src/funding.rs @@ -61,7 +61,7 @@ pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 3; pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 3 * DAYS; #[cfg(feature = "fast-gov")] -pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 4; +pub const SUCCESS_TO_SETTLEMENT_TIME: BlockNumber = 4; #[cfg(not(feature = "fast-gov"))] pub const SUCCESS_TO_SETTLEMENT_TIME: BlockNumber = 4 * DAYS; @@ -88,4 +88,5 @@ parameter_types! { (Percent::from_percent(6), u128::MAX), // Making it max signifies the last bracket ]; pub EarlyEvaluationThreshold: Percent = Percent::from_percent(10); + pub EvaluatorSlash: Percent = Percent::from_percent(20); } diff --git a/runtimes/shared-configuration/src/lib.rs b/runtimes/shared-configuration/src/lib.rs index 01fd24175..f1017f6a2 100644 --- a/runtimes/shared-configuration/src/lib.rs +++ b/runtimes/shared-configuration/src/lib.rs @@ -26,3 +26,11 @@ pub mod weights; /// Common types pub use parachains_common::{Balance, BlockNumber, DAYS}; + +pub use assets::*; +pub use currency::*; +pub use fee::*; +pub use funding::*; +pub use governance::*; +pub use staking::*; +pub use weights::*; diff --git a/runtimes/standalone/src/lib.rs b/runtimes/standalone/src/lib.rs index fad1b29ec..3687db8d3 100644 --- a/runtimes/standalone/src/lib.rs +++ b/runtimes/standalone/src/lib.rs @@ -352,6 +352,7 @@ parameter_types! { (Percent::from_percent(6), u128::MAX), // Making it max signifies the last bracket ]; pub EarlyEvaluationThreshold: Percent = Percent::from_percent(10); + pub TreasuryAccount: AccountId = AccountId::from([69u8; 32]); } impl pallet_funding::Config for Runtime { @@ -366,6 +367,7 @@ impl pallet_funding::Config for Runtime { type EnglishAuctionDuration = EnglishAuctionDuration; type EvaluationDuration = EvaluationDuration; type EvaluationSuccessThreshold = EarlyEvaluationThreshold; + type EvaluatorSlash = EvaluatorSlash; type FeeBrackets = FeeBrackets; type FundingCurrency = Assets; type ManualAcceptanceDuration = ManualAcceptanceDuration; @@ -386,6 +388,7 @@ impl pallet_funding::Config for Runtime { type StorageItemId = u128; type StringLimit = ConstU32<64>; type SuccessToSettlementTime = SuccessToSettlementTime; + type TreasuryAccount = TreasuryAccount; type Vesting = Release; type WeightInfo = (); } diff --git a/runtimes/testnet/src/lib.rs b/runtimes/testnet/src/lib.rs index 3a7d9a335..6bc6ffb3e 100644 --- a/runtimes/testnet/src/lib.rs +++ b/runtimes/testnet/src/lib.rs @@ -68,15 +68,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); // Polimec Shared Imports use pallet_funding::BondTypeOf; pub use pallet_parachain_staking; -pub use shared_configuration::{ - assets::*, - currency::{vesting::*, *}, - fee::*, - funding::*, - governance::*, - staking::*, - weights::*, -}; +pub use shared_configuration::*; pub type NegativeImbalanceOf = as Currency<::AccountId>>::NegativeImbalance; @@ -487,6 +479,9 @@ impl pallet_assets::Config for Runtime { type WeightInfo = (); } +parameter_types! { + pub TreasuryAccount: AccountId = [69u8; 32].into(); +} impl pallet_funding::Config for Runtime { type AuctionInitializePeriodDuration = AuctionInitializePeriodDuration; type Balance = Balance; @@ -499,6 +494,7 @@ impl pallet_funding::Config for Runtime { type EnglishAuctionDuration = EnglishAuctionDuration; type EvaluationDuration = EvaluationDuration; type EvaluationSuccessThreshold = EarlyEvaluationThreshold; + type EvaluatorSlash = EvaluatorSlash; type FeeBrackets = FeeBrackets; type FundingCurrency = StatemintAssets; type ManualAcceptanceDuration = ManualAcceptanceDuration; @@ -519,6 +515,7 @@ impl pallet_funding::Config for Runtime { type StorageItemId = u128; type StringLimit = ConstU32<64>; type SuccessToSettlementTime = SuccessToSettlementTime; + type TreasuryAccount = TreasuryAccount; type Vesting = Vesting; type WeightInfo = (); }