diff --git a/integration-tests/src/tests/ct_migration.rs b/integration-tests/src/tests/ct_migration.rs index 6fbbf84d7..4f28ac483 100644 --- a/integration-tests/src/tests/ct_migration.rs +++ b/integration-tests/src/tests/ct_migration.rs @@ -16,13 +16,18 @@ use crate::*; use frame_support::traits::{fungible::Mutate, fungibles::Inspect}; +use itertools::Itertools; use pallet_funding::{assert_close_enough, types::*, ProjectId}; -use polimec_common::migration_types::{MigrationStatus, Migrations}; -use polimec_runtime::Funding; +use polimec_common::migration_types::{MigrationStatus, Migrations, ParticipationType}; +use polimec_runtime::{Funding, RuntimeOrigin}; +use polkadot_service::chain_spec::get_account_id_from_seed; use sp_runtime::Perquintill; use std::collections::HashMap; use tests::defaults::*; +fn alice() -> AccountId { + get_account_id_from_seed::(ALICE) +} fn mock_hrmp_establishment(project_id: u32) { let ct_issued = PolimecNet::execute_with(|| { ::ContributionTokenCurrency::total_issuance(project_id) @@ -61,6 +66,16 @@ fn assert_migration_is_ready(project_id: u32) { }); } +fn assert_migration_not_ready(project_id: u32) { + PolimecNet::execute_with(|| { + let project_details = pallet_funding::ProjectsDetails::::get(project_id).unwrap(); + let MigrationType::ParachainReceiverPallet(receiver_pallet_info) = project_details.migration_type else { + panic!("Migration type is not ParachainReceiverPallet"); + }; + assert!(!receiver_pallet_info.migration_readiness_check.unwrap().is_ready()) + }); +} + fn get_migrations_for_participants( project_id: ProjectId, participants: Vec, @@ -181,10 +196,12 @@ fn create_settled_project() -> (ProjectId, Vec) { } #[test] -fn full_migration_test() { +fn full_receiver_pallet_migration_test() { polimec::set_prices(); let (project_id, participants) = create_settled_project(); - + let project_status = + PolimecNet::execute_with(|| pallet_funding::ProjectsDetails::::get(project_id).unwrap().status); + dbg!(project_status); mock_hrmp_establishment(project_id); assert_migration_is_ready(project_id); @@ -200,3 +217,113 @@ fn full_migration_test() { migrations_are_vested(project_id, participants.clone()); } + +/// Creates a project with all participations settled except for one. +fn create_project_with_unsettled_participation(participation_type: ParticipationType) -> (ProjectId, Vec) { + let mut inst = IntegrationInstantiator::new(None); + PolimecNet::execute_with(|| { + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER.into()), + ISSUER.into(), + default_evaluations(), + default_bids(), + default_community_contributions(), + default_remainder_contributions(), + ); + + inst.advance_time(::SuccessToSettlementTime::get()).unwrap(); + let evaluations_to_settle = + pallet_funding::Evaluations::::iter_prefix_values((project_id,)).collect_vec(); + let bids_to_settle = pallet_funding::Bids::::iter_prefix_values((project_id,)).collect_vec(); + let contributions_to_settle = + pallet_funding::Contributions::::iter_prefix_values((project_id,)).collect_vec(); + + let mut participants: Vec = evaluations_to_settle + .iter() + .map(|eval| eval.evaluator.clone()) + .chain(bids_to_settle.iter().map(|bid| bid.bidder.clone())) + .chain(contributions_to_settle.iter().map(|contribution| contribution.contributor.clone())) + .collect(); + participants.sort(); + participants.dedup(); + + let start = if participation_type == ParticipationType::Evaluation { 1 } else { 0 }; + for evaluation in evaluations_to_settle[start..].iter() { + PolimecFunding::settle_successful_evaluation( + RuntimeOrigin::signed(alice()), + project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) + .unwrap() + } + + let start = if participation_type == ParticipationType::Bid { 1 } else { 0 }; + for bid in bids_to_settle[start..].iter() { + PolimecFunding::settle_successful_bid( + RuntimeOrigin::signed(alice()), + project_id, + bid.bidder.clone(), + bid.id, + ) + .unwrap() + } + + let start = if participation_type == ParticipationType::Contribution { 1 } else { 0 }; + for contribution in contributions_to_settle[start..].iter() { + PolimecFunding::settle_successful_contribution( + RuntimeOrigin::signed(alice()), + project_id, + contribution.contributor.clone(), + contribution.id, + ) + .unwrap() + } + + let evaluations = + pallet_funding::Evaluations::::iter_prefix_values((project_id,)).collect_vec(); + let bids = pallet_funding::Bids::::iter_prefix_values((project_id,)).collect_vec(); + let contributions = + pallet_funding::Contributions::::iter_prefix_values((project_id,)).collect_vec(); + + if participation_type == ParticipationType::Evaluation { + assert_eq!(evaluations.len(), 1); + assert_eq!(bids.len(), 0); + assert_eq!(contributions.len(), 0); + } else if participation_type == ParticipationType::Bid { + assert_eq!(evaluations.len(), 0); + assert_eq!(bids.len(), 1); + assert_eq!(contributions.len(), 0); + } else { + assert_eq!(evaluations.len(), 0); + assert_eq!(bids.len(), 0); + assert_eq!(contributions.len(), 1); + } + + (project_id, participants) + }) +} + +#[test] +fn cannot_start_pallet_migration_with_unsettled_participations() { + polimec::set_prices(); + + let tup_1 = create_project_with_unsettled_participation(ParticipationType::Evaluation); + let tup_2 = create_project_with_unsettled_participation(ParticipationType::Bid); + let tup_3 = create_project_with_unsettled_participation(ParticipationType::Contribution); + + let tups = vec![tup_1, tup_2, tup_3]; + + for (project_id, participants) in tups.into_iter() { + PolimecNet::execute_with(|| { + assert_noop!( + PolimecFunding::do_configure_receiver_pallet_migration( + &ISSUER.into(), + project_id, + ParaId::from(6969u32) + ), + pallet_funding::Error::::SettlementNotComplete + ); + }); + } +} diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index 524846841..32c51e92a 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -38,9 +38,6 @@ impl Pallet { // * Calculate new variables * project_details.funding_end_block = Some(now); - // * Update storage * - ProjectsDetails::::insert(project_id, &project_details); - let escrow_account = Self::fund_account_id(project_id); if project_details.status == ProjectStatus::FundingSuccessful { T::ContributionTokenCurrency::create(project_id, escrow_account.clone(), false, 1_u32.into())?; @@ -73,11 +70,17 @@ impl Pallet { liquidity_pools_ct_amount, )?; + project_details.status = ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful); + ProjectsDetails::::insert(project_id, &project_details); + Ok(PostDispatchInfo { actual_weight: Some(WeightInfoOf::::start_settlement_funding_success()), pays_fee: Pays::Yes, }) } else { + project_details.status = ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed); + ProjectsDetails::::insert(project_id, &project_details); + Ok(PostDispatchInfo { actual_weight: Some(WeightInfoOf::::start_settlement_funding_failure()), pays_fee: Pays::Yes, @@ -87,8 +90,14 @@ impl Pallet { pub fn do_settle_successful_evaluation(evaluation: EvaluationInfoOf, project_id: ProjectId) -> DispatchResult { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - ensure!(matches!(project_details.funding_end_block, Some(_)), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(_)), + Error::::SettlementNotStarted + ); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful)), + Error::::WrongSettlementOutcome + ); // Based on the results of the funding round, the evaluator is either: // 1. Slashed @@ -137,8 +146,14 @@ impl Pallet { pub fn do_settle_failed_evaluation(evaluation: EvaluationInfoOf, project_id: ProjectId) -> DispatchResult { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - ensure!(matches!(project_details.funding_end_block, Some(_)), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingFailed), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(_)), + Error::::SettlementNotStarted + ); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)), + Error::::WrongSettlementOutcome + ); let bond = if matches!(project_details.evaluation_round_info.evaluators_outcome, EvaluatorsOutcome::Slashed) { Self::slash_evaluator(project_id, &evaluation)? @@ -171,8 +186,14 @@ impl Pallet { let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - ensure!(matches!(project_details.funding_end_block, Some(_)), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(_)), + Error::::SettlementNotStarted + ); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful)), + Error::::WrongSettlementOutcome + ); ensure!( matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..)), Error::::ImpossibleState @@ -234,8 +255,14 @@ impl Pallet { pub fn do_settle_failed_bid(bid: BidInfoOf, project_id: ProjectId) -> DispatchResult { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - ensure!(matches!(project_details.funding_end_block, Some(_)), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingFailed), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(_)), + Error::::SettlementNotStarted + ); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)), + Error::::WrongSettlementOutcome + ); let bidder = bid.bidder; @@ -262,8 +289,14 @@ impl Pallet { // Ensure that: // 1. The project is in the FundingSuccessful state // 2. The contribution token exists - ensure!(matches!(project_details.funding_end_block, Some(_)), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(_)), + Error::::SettlementNotStarted + ); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful)), + Error::::WrongSettlementOutcome + ); ensure!(T::ContributionTokenCurrency::asset_exists(project_id), Error::::TooEarlyForRound); let contributor = contribution.contributor; @@ -321,8 +354,15 @@ impl Pallet { pub fn do_settle_failed_contribution(contribution: ContributionInfoOf, project_id: ProjectId) -> DispatchResult { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - ensure!(matches!(project_details.funding_end_block, Some(_)), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingFailed), Error::::WrongSettlementOutcome); + + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(_)), + Error::::SettlementNotStarted + ); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)), + Error::::WrongSettlementOutcome + ); // Check if the bidder has a future deposit held let contributor = contribution.contributor; @@ -351,6 +391,31 @@ impl Pallet { Ok(()) } + pub fn do_mark_project_as_settled(project_id: ProjectId) -> DispatchResult { + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + let outcome = match project_details.status { + ProjectStatus::SettlementStarted(outcome) => outcome, + _ => return Err(Error::::SettlementNotStarted.into()), + }; + + // We use closers to do an early return if just one of these storage iterators returns a value. + let no_evaluations_remaining = || Evaluations::::iter_prefix((project_id,)).next().is_none(); + let no_bids_remaining = || Bids::::iter_prefix((project_id,)).next().is_none(); + let no_contributions_remaining = || Contributions::::iter_prefix((project_id,)).next().is_none(); + + // Check if there are any evaluations, bids or contributions remaining + ensure!( + no_evaluations_remaining() && no_bids_remaining() && no_contributions_remaining(), + Error::::SettlementNotComplete + ); + + // Mark the project as settled + project_details.status = ProjectStatus::SettlementFinished(outcome); + ProjectsDetails::::insert(project_id, project_details); + + Ok(()) + } + fn mint_contribution_tokens( project_id: ProjectId, participant: &AccountIdOf, diff --git a/pallets/funding/src/functions/7_ct_migration.rs b/pallets/funding/src/functions/7_ct_migration.rs index 6689eb4bb..029fbb59f 100644 --- a/pallets/funding/src/functions/7_ct_migration.rs +++ b/pallets/funding/src/functions/7_ct_migration.rs @@ -1,4 +1,5 @@ use super::*; +use crate::ProjectStatus::SettlementFinished; use xcm::v3::MaxPalletNameLen; impl Pallet { @@ -13,7 +14,12 @@ impl Pallet { // * Validity checks * ensure!(&(project_details.issuer_account) == caller, Error::::NotIssuer); - ensure!(project_details.status == ProjectStatus::FundingSuccessful, Error::::IncorrectRound); + match project_details.status { + ProjectStatus::SettlementFinished(FundingOutcome::FundingSuccessful) => (), + ProjectStatus::FundingSuccessful | ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) => + return Err(Error::::SettlementNotComplete.into()), + _ => return Err(Error::::IncorrectRound.into()), + } // * Update storage * let parachain_receiver_pallet_info = ParachainReceiverPalletInfo { @@ -63,7 +69,7 @@ impl Pallet { matches!( &details.migration_type, MigrationType::ParachainReceiverPallet(info) if - info.parachain_id == ParaId::from(sender) && details.status == FundingSuccessful) + info.parachain_id == ParaId::from(sender) && details.status == SettlementFinished(FundingOutcome::FundingSuccessful)) }) .ok_or(XcmError::BadOrigin)?; @@ -147,7 +153,7 @@ impl Pallet { matches!( &details.migration_type, MigrationType::ParachainReceiverPallet(info) if - info.parachain_id == ParaId::from(recipient) && details.status == FundingSuccessful) + info.parachain_id == ParaId::from(recipient) && details.status == SettlementFinished(FundingOutcome::FundingSuccessful)) }) .ok_or(XcmError::BadOrigin)?; @@ -195,7 +201,10 @@ impl Pallet { let max_weight = Weight::from_parts(700_000_000, 10_000); // * Validity checks * - ensure!(project_details.status == ProjectStatus::FundingSuccessful, Error::::IncorrectRound); + ensure!( + project_details.status == SettlementFinished(FundingOutcome::FundingSuccessful), + Error::::IncorrectRound + ); ensure!( migration_info.hrmp_channel_status == HRMPChannelStatus { @@ -208,7 +217,7 @@ impl Pallet { ensure!(caller.clone() == T::PalletId::get().into_account_truncating(), Error::::NotAllowed); } else if matches!( migration_info.migration_readiness_check, - Some(MigrationReadinessCheck { + Some(PalletMigrationReadinessCheck { holding_check: (_, CheckOutcome::Failed), pallet_check: (_, CheckOutcome::Failed), .. @@ -233,7 +242,7 @@ impl Pallet { Here, ); - migration_info.migration_readiness_check = Some(MigrationReadinessCheck { + migration_info.migration_readiness_check = Some(PalletMigrationReadinessCheck { holding_check: (query_id_holdings, CheckOutcome::AwaitingResponse), pallet_check: (query_id_pallet, CheckOutcome::AwaitingResponse), }); @@ -311,7 +320,10 @@ impl Pallet { ( Response::Assets(assets), &mut Some( - ref mut check @ MigrationReadinessCheck { holding_check: (_, CheckOutcome::AwaitingResponse), .. }, + ref mut check @ PalletMigrationReadinessCheck { + holding_check: (_, CheckOutcome::AwaitingResponse), + .. + }, ), ) => { let ct_sold_as_u128: u128 = contribution_tokens_sold.try_into().map_err(|_| Error::::BadMath)?; @@ -342,7 +354,12 @@ impl Pallet { ( Response::PalletsInfo(pallets_info), - Some(ref mut check @ MigrationReadinessCheck { pallet_check: (_, CheckOutcome::AwaitingResponse), .. }), + Some( + ref mut check @ PalletMigrationReadinessCheck { + pallet_check: (_, CheckOutcome::AwaitingResponse), + .. + }, + ), ) => { let expected_module_name: BoundedVec = BoundedVec::try_from("polimec_receiver".as_bytes().to_vec()).map_err(|_| Error::::NotAllowed)?; @@ -392,7 +409,8 @@ impl Pallet { let project_multilocation = MultiLocation { parents: 1, interior: X1(Parachain(project_para_id.into())) }; let call: ::RuntimeCall = - Call::confirm_migrations { query_id: Default::default(), response: Default::default() }.into(); + Call::confirm_receiver_pallet_migrations { query_id: Default::default(), response: Default::default() } + .into(); let query_id = pallet_xcm::Pallet::::new_notify_query(project_multilocation, call.into(), now + 20u32.into(), Here); @@ -419,7 +437,11 @@ impl Pallet { /// Mark the migration item that corresponds to a single participation as confirmed or failed. #[transactional] - pub fn do_confirm_migrations(location: MultiLocation, query_id: QueryId, response: Response) -> DispatchResult { + pub fn do_confirm_receiver_pallet_migrations( + location: MultiLocation, + query_id: QueryId, + response: Response, + ) -> DispatchResult { use xcm::v3::prelude::*; let (project_id, participant) = ActiveMigrationQueue::::take(query_id).ok_or(Error::::NoActiveMigrationsFound)?; diff --git a/pallets/funding/src/functions/mod.rs b/pallets/funding/src/functions/mod.rs index c058cfe08..d7db28bb8 100644 --- a/pallets/funding/src/functions/mod.rs +++ b/pallets/funding/src/functions/mod.rs @@ -1,9 +1,6 @@ use super::*; -use crate::{ - traits::{BondingRequirementCalculation, ProvideAssetPrice, VestingDurationCalculation}, - ProjectStatus::FundingSuccessful, -}; +use crate::traits::{BondingRequirementCalculation, ProvideAssetPrice, VestingDurationCalculation}; use core::ops::Not; use frame_support::{ dispatch::{DispatchErrorWithPostInfo, DispatchResult, DispatchResultWithPostInfo, PostDispatchInfo}, diff --git a/pallets/funding/src/instantiator/chain_interactions.rs b/pallets/funding/src/instantiator/chain_interactions.rs index b72f141ea..1d70a06e9 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -717,10 +717,16 @@ impl< pub fn settle_project(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { let details = self.get_project_details(project_id); match details.status { - ProjectStatus::FundingSuccessful => self.settle_successful_project(project_id), - ProjectStatus::FundingFailed => self.settle_failed_project(project_id), + ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) => + self.settle_successful_project(project_id).unwrap(), + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) => + self.settle_failed_project(project_id).unwrap(), _ => panic!("Project should be in FundingSuccessful or FundingFailed status"), } + self.execute(|| { + crate::Pallet::::do_mark_project_as_settled(project_id).unwrap(); + }); + Ok(()) } fn settle_successful_project(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index 4f90bce0b..47fcf548c 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -816,6 +816,8 @@ pub mod pallet { WrongSettlementOutcome, /// User still has participations that need to be settled before migration. ParticipationsNotSettled, + /// Tried to mark project as fully settled but there are participations that are not settled. + SettlementNotComplete, } #[pallet::call] @@ -1150,10 +1152,26 @@ pub mod pallet { #[pallet::call_index(27)] #[pallet::weight(Weight::from_parts(1000, 0))] - pub fn confirm_migrations(origin: OriginFor, query_id: QueryId, response: Response) -> DispatchResult { + pub fn confirm_receiver_pallet_migrations( + origin: OriginFor, + query_id: QueryId, + response: Response, + ) -> DispatchResult { + let location = ensure_response(::RuntimeOrigin::from(origin))?; + + Self::do_confirm_receiver_pallet_migrations(location, query_id, response) + } + + #[pallet::call_index(38)] + #[pallet::weight(Weight::from_parts(1000, 0))] + pub fn confirm_offchain_migration( + origin: OriginFor, + query_id: QueryId, + response: Response, + ) -> DispatchResult { let location = ensure_response(::RuntimeOrigin::from(origin))?; - Self::do_confirm_migrations(location, query_id, response) + Self::do_confirm_receiver_pallet_migrations(location, query_id, response) } #[pallet::call_index(28)] diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index c9080d8fe..c1d84eae6 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -307,7 +307,7 @@ pub mod storage_types { /// HRMP Channel status pub hrmp_channel_status: HRMPChannelStatus, /// Migration readiness check - pub migration_readiness_check: Option, + pub migration_readiness_check: Option, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] @@ -676,8 +676,16 @@ pub mod inner_types { FundingFailed, AwaitingProjectDecision, FundingSuccessful, - ReadyToStartMigration, - MigrationCompleted, + SettlementStarted(FundingOutcome), + SettlementFinished(FundingOutcome), + CTMigrationStarted, + CTMigrationFinished, + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Serialize, Deserialize)] + pub enum FundingOutcome { + FundingSuccessful, + FundingFailed, } #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] @@ -838,12 +846,12 @@ pub mod inner_types { } #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct MigrationReadinessCheck { + pub struct PalletMigrationReadinessCheck { pub holding_check: (xcm::v3::QueryId, CheckOutcome), pub pallet_check: (xcm::v3::QueryId, CheckOutcome), } - impl MigrationReadinessCheck { + impl PalletMigrationReadinessCheck { pub fn is_ready(&self) -> bool { self.holding_check.1 == CheckOutcome::Passed(None) && matches!(self.pallet_check.1, CheckOutcome::Passed(Some(_)))