diff --git a/integration-tests/src/tests/ct_migration.rs b/integration-tests/src/tests/ct_migration.rs index 9a97323b5..46ec9709b 100644 --- a/integration-tests/src/tests/ct_migration.rs +++ b/integration-tests/src/tests/ct_migration.rs @@ -16,13 +16,19 @@ use crate::*; use frame_support::traits::{fungible::Mutate, fungibles::Inspect}; -use pallet_funding::{assert_close_enough, ProjectId}; -use polimec_common::migration_types::{MigrationStatus, Migrations}; -use polimec_runtime::Funding; +use itertools::Itertools; +use pallet_funding::{assert_close_enough, types::*, ProjectId}; +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) @@ -34,7 +40,7 @@ fn mock_hrmp_establishment(project_id: u32) { }); PolimecNet::execute_with(|| { - assert_ok!(Funding::do_set_para_id_for_project(&ISSUER.into(), project_id, ParaId::from(6969u32))); + assert_ok!(Funding::do_start_pallet_migration(&ISSUER.into(), project_id, ParaId::from(6969u32))); let open_channel_message = xcm::v3::opaque::Instruction::HrmpNewChannelOpenRequest { sender: 6969, @@ -54,7 +60,10 @@ fn mock_hrmp_establishment(project_id: u32) { fn assert_migration_is_ready(project_id: u32) { PolimecNet::execute_with(|| { let project_details = pallet_funding::ProjectsDetails::::get(project_id).unwrap(); - assert!(project_details.migration_readiness_check.unwrap().is_ready()) + let Some(MigrationType::Pallet(receiver_pallet_info)) = project_details.migration_type else { + panic!("Migration type is not ParachainReceiverPallet"); + }; + assert!(receiver_pallet_info.migration_readiness_check.unwrap().is_ready()) }); } @@ -66,7 +75,7 @@ fn get_migrations_for_participants( PolimecNet::execute_with(|| { for participant in participants { let (status, migrations) = - pallet_funding::UserMigrations::::get(project_id, participant.clone()).unwrap(); + pallet_funding::UserMigrations::::get((project_id, participant.clone())).unwrap(); user_migrations.insert(participant, (status, Migrations::from(migrations.into()))); } }); @@ -76,7 +85,11 @@ fn get_migrations_for_participants( fn send_migrations(project_id: ProjectId, accounts: Vec) { for user in accounts.into_iter() { PolimecNet::execute_with(|| { - assert_ok!(Funding::migrate_one_participant(PolimecOrigin::signed(user.clone()), project_id, user.clone())); + assert_ok!(Funding::send_pallet_migration_for( + PolimecOrigin::signed(user.clone()), + project_id, + user.clone() + )); }); } } @@ -114,6 +127,10 @@ fn migrations_are_confirmed(project_id: u32, accounts: Vec) { let (current_status, _) = user_migrations.get(user).unwrap(); assert_eq!(current_status, &MigrationStatus::Confirmed); } + + PolimecFunding::do_mark_project_ct_migration_as_finished(project_id).unwrap(); + let project_details = pallet_funding::ProjectsDetails::::get(project_id).unwrap(); + assert_eq!(project_details.status, pallet_funding::ProjectStatus::CTMigrationFinished) }); } @@ -174,15 +191,17 @@ fn create_settled_project() -> (ProjectId, Vec) { } #[test] -fn full_migration_test() { +fn full_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); - // Migrate is sent send_migrations(project_id, participants.clone()); migrations_are_executed(project_id, participants.clone()); @@ -193,3 +212,109 @@ 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_start_pallet_migration(&ISSUER.into(), project_id, ParaId::from(6969u32)), + pallet_funding::Error::::SettlementNotComplete + ); + }); + } +} diff --git a/integration-tests/src/tests/e2e.rs b/integration-tests/src/tests/e2e.rs index 744cb30a5..c3db8eae7 100644 --- a/integration-tests/src/tests/e2e.rs +++ b/integration-tests/src/tests/e2e.rs @@ -485,7 +485,7 @@ fn ct_migrated() { // Mock HRMP establishment PolimecNet::execute_with(|| { let _account_id: PolimecAccountId = ISSUER.into(); - assert_ok!(PolimecFunding::do_set_para_id_for_project(&ISSUER.into(), project_id, ParaId::from(6969u32),)); + assert_ok!(PolimecFunding::do_start_pallet_migration(&ISSUER.into(), project_id, ParaId::from(6969u32),)); let open_channel_message = xcm::v3::opaque::Instruction::HrmpNewChannelOpenRequest { sender: 6969, max_message_size: 102_300, @@ -505,7 +505,10 @@ fn ct_migrated() { // Migration is ready PolimecNet::execute_with(|| { let project_details = pallet_funding::ProjectsDetails::::get(project_id).unwrap(); - assert!(project_details.migration_readiness_check.unwrap().is_ready()) + let Some(MigrationType::Pallet(migration_info)) = project_details.migration_type else { + panic!("Migration type should be ParachainReceiverPallet"); + }; + assert!(migration_info.migration_readiness_check.unwrap().is_ready()) }); excel_ct_amounts().iter().map(|tup| tup.0.clone()).unique().for_each(|account| { @@ -525,7 +528,7 @@ fn ct_migrated() { for account in accounts { PolimecNet::execute_with(|| { - assert_ok!(PolimecFunding::migrate_one_participant( + assert_ok!(PolimecFunding::send_pallet_migration_for( PolimecOrigin::signed(account.clone()), project_id, account.clone() diff --git a/pallets/funding/src/benchmarking.rs b/pallets/funding/src/benchmarking.rs index 0aa3ab12e..6a6e3cab5 100644 --- a/pallets/funding/src/benchmarking.rs +++ b/pallets/funding/src/benchmarking.rs @@ -1535,7 +1535,10 @@ mod benchmarks { inst.create_finished_project(project_metadata, issuer, evaluations, bids, contributions, vec![]); inst.advance_time(One::one()).unwrap(); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); let evaluation_to_settle = inst.execute(|| Evaluations::::iter_prefix_values((project_id, evaluator.clone())).next().unwrap()); @@ -1606,7 +1609,10 @@ mod benchmarks { run_blocks_to_execute_next_transition(project_id, UpdateType::StartSettlement, &mut inst); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingSuccessful); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) + ); let bid_to_settle = inst.execute(|| Bids::::iter_prefix_values((project_id, bidder.clone())).next().unwrap()); @@ -1665,7 +1671,10 @@ mod benchmarks { inst.create_finished_project(project_metadata, issuer.clone(), evaluations, bids, contributions, vec![]); inst.advance_time(One::one()).unwrap(); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); let bid_to_settle = inst.execute(|| Bids::::iter_prefix_values((project_id, bidder.clone())).next().unwrap()); @@ -1713,7 +1722,10 @@ mod benchmarks { run_blocks_to_execute_next_transition(project_id, UpdateType::StartSettlement, &mut inst); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingSuccessful); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) + ); let contribution_to_settle = inst.execute(|| Contributions::::iter_prefix_values((project_id, contributor.clone())).next().unwrap()); @@ -1783,7 +1795,10 @@ mod benchmarks { inst.create_finished_project(project_metadata, issuer, evaluations, bids, contributions, vec![]); inst.advance_time(One::one()).unwrap(); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); let contribution_to_settle = inst.execute(|| Contributions::::iter_prefix_values((project_id, contributor.clone())).next().unwrap()); @@ -2618,7 +2633,7 @@ mod benchmarks { // * validity checks * let project_details = inst.get_project_details(project_id); - assert_eq!(project_details.status, ProjectStatus::FundingSuccessful); + assert_eq!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful)); } #[benchmark] @@ -2667,7 +2682,7 @@ mod benchmarks { // * validity checks * let project_details = inst.get_project_details(project_id); - assert_eq!(project_details.status, ProjectStatus::FundingFailed); + assert_eq!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)); } #[cfg(test)] diff --git a/pallets/funding/src/functions/1_application.rs b/pallets/funding/src/functions/1_application.rs index 27498df80..f40fbb1a6 100644 --- a/pallets/funding/src/functions/1_application.rs +++ b/pallets/funding/src/functions/1_application.rs @@ -46,12 +46,7 @@ impl Pallet { }, usd_bid_on_oversubscription: None, funding_end_block: None, - parachain_id: None, - migration_readiness_check: None, - hrmp_channel_status: HRMPChannelStatus { - project_to_polimec: ChannelStatus::Closed, - polimec_to_project: ChannelStatus::Closed, - }, + migration_type: None, }; let bucket: BucketOf = Self::create_bucket_from_metadata(&project_metadata)?; diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index 48a4d3622..e60590575 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,11 @@ 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!(project_details.funding_end_block.is_some(), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::WrongSettlementOutcome); + + ensure!( + project_details.status == ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful), + Error::::FundingSuccessSettlementNotStarted + ); // Based on the results of the funding round, the evaluator is either: // 1. Slashed @@ -137,8 +143,10 @@ 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!(project_details.funding_end_block.is_some(), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingFailed), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)), + Error::::FundingFailedSettlementNotStarted + ); let bond = if matches!(project_details.evaluation_round_info.evaluators_outcome, EvaluatorsOutcome::Slashed) { Self::slash_evaluator(project_id, &evaluation)? @@ -171,8 +179,10 @@ 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!(project_details.funding_end_block.is_some(), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::WrongSettlementOutcome); + ensure!( + project_details.status == ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful), + Error::::FundingSuccessSettlementNotStarted + ); ensure!( matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..)), Error::::ImpossibleState @@ -234,8 +244,10 @@ 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!(project_details.funding_end_block.is_some(), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingFailed), Error::::WrongSettlementOutcome); + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)), + Error::::FundingFailedSettlementNotStarted + ); let bidder = bid.bidder; @@ -262,8 +274,10 @@ impl Pallet { // Ensure that: // 1. The project is in the FundingSuccessful state // 2. The contribution token exists - ensure!(project_details.funding_end_block.is_some(), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::WrongSettlementOutcome); + ensure!( + project_details.status == ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful), + Error::::FundingSuccessSettlementNotStarted + ); ensure!(T::ContributionTokenCurrency::asset_exists(project_id), Error::::TooEarlyForRound); let contributor = contribution.contributor; @@ -321,8 +335,11 @@ 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!(project_details.funding_end_block.is_some(), Error::::SettlementNotStarted); - ensure!(matches!(project_details.status, ProjectStatus::FundingFailed), Error::::WrongSettlementOutcome); + + ensure!( + matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)), + Error::::FundingFailedSettlementNotStarted + ); // Check if the bidder has a future deposit held let contributor = contribution.contributor; @@ -351,6 +368,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::::IncorrectRound.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, @@ -447,7 +489,7 @@ impl Pallet { ct_amount: BalanceOf, vesting_time: BlockNumberFor, ) -> DispatchResult { - UserMigrations::::try_mutate(project_id, origin, |maybe_migrations| -> DispatchResult { + UserMigrations::::try_mutate((project_id, origin), |maybe_migrations| -> DispatchResult { let multilocation_user = MultiLocation::new( 0, X1(AccountId32 { network: None, id: T::AccountId32Conversion::convert(origin.clone()) }), @@ -461,7 +503,9 @@ impl Pallet { } else { let mut migrations = BoundedVec::<_, MaxParticipationsPerUser>::new(); migrations.try_push(migration).map_err(|_| Error::::TooManyMigrations)?; - *maybe_migrations = Some((MigrationStatus::NotStarted, migrations)) + *maybe_migrations = Some((MigrationStatus::NotStarted, migrations)); + + UnmigratedCounter::::mutate(project_id, |counter| *counter = counter.saturating_sub(1)); } Ok(()) diff --git a/pallets/funding/src/functions/7_ct_migration.rs b/pallets/funding/src/functions/7_ct_migration.rs index 1c2f6656e..3c37e0a04 100644 --- a/pallets/funding/src/functions/7_ct_migration.rs +++ b/pallets/funding/src/functions/7_ct_migration.rs @@ -1,9 +1,51 @@ use super::*; use xcm::v3::MaxPalletNameLen; +// Offchain migration functions impl Pallet { #[transactional] - pub fn do_set_para_id_for_project( + pub fn do_start_offchain_migration(project_id: ProjectId, caller: AccountIdOf) -> DispatchResult { + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + + ensure!(project_details.issuer_account == caller, Error::::NotIssuer); + match project_details.status { + ProjectStatus::SettlementFinished(FundingOutcome::FundingSuccessful) => (), + ProjectStatus::FundingSuccessful | ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) => + return Err(Error::::SettlementNotComplete.into()), + _ => return Err(Error::::IncorrectRound.into()), + } + + project_details.migration_type = Some(MigrationType::Offchain); + project_details.status = ProjectStatus::CTMigrationStarted; + ProjectsDetails::::insert(project_id, project_details); + + // * Emit events * + Ok(()) + } + + #[transactional] + pub fn do_confirm_offchain_migration( + project_id: ProjectId, + caller: AccountIdOf, + participant: AccountIdOf, + ) -> DispatchResult { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + // * Validity checks * + ensure!(project_details.status == ProjectStatus::CTMigrationStarted, Error::::IncorrectRound); + ensure!(project_details.issuer_account == caller, Error::::NotIssuer); + + // * Update storage * + Self::change_migration_status(project_id, participant.clone(), MigrationStatus::Confirmed)?; + + Ok(()) + } +} + +// Pallet migration functions +impl Pallet { + #[transactional] + pub fn do_start_pallet_migration( caller: &AccountIdOf, project_id: ProjectId, para_id: ParaId, @@ -13,13 +55,28 @@ impl Pallet { // * Validity checks * ensure!(&(project_details.issuer_account) == caller, Error::::NotIssuer); + 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 * - project_details.parachain_id = Some(para_id); + let parachain_receiver_pallet_info = PalletMigrationInfo { + parachain_id: para_id, + hrmp_channel_status: HRMPChannelStatus { + project_to_polimec: ChannelStatus::Closed, + polimec_to_project: ChannelStatus::Closed, + }, + migration_readiness_check: None, + }; + project_details.migration_type = Some(MigrationType::Pallet(parachain_receiver_pallet_info)); + project_details.status = ProjectStatus::CTMigrationStarted; ProjectsDetails::::insert(project_id, project_details); // * Emit events * - Self::deposit_event(Event::ProjectParaIdSet { project_id, para_id }); + Self::deposit_event(Event::PalletMigrationStarted { project_id, para_id }); Ok(()) } @@ -51,7 +108,10 @@ impl Pallet { let (project_id, mut project_details) = ProjectsDetails::::iter() .find(|(_id, details)| { - details.parachain_id == Some(ParaId::from(sender)) && details.status == FundingSuccessful + matches!( + &details.migration_type, + Some(MigrationType::Pallet(info)) if + info.parachain_id == ParaId::from(sender) && details.status == ProjectStatus::CTMigrationStarted) }) .ok_or(XcmError::BadOrigin)?; @@ -95,8 +155,14 @@ impl Pallet { match T::XcmRouter::deliver(ticket) { Ok(_) => { log::trace!(target: "pallet_funding::hrmp", "HrmpNewChannelOpenRequest: acceptance successfully sent"); - project_details.hrmp_channel_status.project_to_polimec = ChannelStatus::Open; - project_details.hrmp_channel_status.polimec_to_project = ChannelStatus::AwaitingAcceptance; + match project_details.migration_type { + Some(MigrationType::Pallet(ref mut info)) => { + info.hrmp_channel_status.project_to_polimec = ChannelStatus::Open; + info.hrmp_channel_status.polimec_to_project = ChannelStatus::AwaitingAcceptance; + }, + _ => return Err(XcmError::Transport("Migration type not set")), + } + ProjectsDetails::::insert(project_id, project_details); Pallet::::deposit_event(Event::::HrmpChannelAccepted { @@ -126,11 +192,20 @@ impl Pallet { log::trace!(target: "pallet_funding::hrmp", "HrmpChannelAccepted received: {:?}", message); let (project_id, mut project_details) = ProjectsDetails::::iter() .find(|(_id, details)| { - details.parachain_id == Some(ParaId::from(recipient)) && details.status == FundingSuccessful + matches!( + &details.migration_type, + Some(MigrationType::Pallet(info)) if + info.parachain_id == ParaId::from(recipient) && details.status == ProjectStatus::CTMigrationStarted) }) .ok_or(XcmError::BadOrigin)?; - project_details.hrmp_channel_status.polimec_to_project = ChannelStatus::Open; + match project_details.migration_type { + Some(MigrationType::Pallet(ref mut info)) => { + info.hrmp_channel_status.polimec_to_project = ChannelStatus::Open; + }, + _ => return Err(XcmError::Transport("Unexpected automatic flow")), + } + ProjectsDetails::::insert(project_id, project_details); Pallet::::deposit_event(Event::::HrmpChannelEstablished { project_id, @@ -141,7 +216,7 @@ impl Pallet { &(T::PalletId::get().into_account_truncating()), project_id, ) - .map_err(|_| XcmError::NoDeal)?; + .map_err(|_| XcmError::Transport("Unexpected automatic flow"))?; Ok(()) }, instr => { @@ -157,7 +232,10 @@ impl Pallet { pub fn do_start_migration_readiness_check(caller: &AccountIdOf, project_id: ProjectId) -> DispatchResult { // * Get variables * let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - let parachain_id: u32 = project_details.parachain_id.ok_or(Error::::ImpossibleState)?.into(); + let Some(MigrationType::Pallet(ref mut migration_info)) = project_details.migration_type else { + return Err(Error::::NotAllowed.into()) + }; + let parachain_id: u32 = migration_info.parachain_id.into(); let project_multilocation = ParentThen(X1(Parachain(parachain_id))); let now = >::block_number(); @@ -165,20 +243,20 @@ 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 == ProjectStatus::CTMigrationStarted, Error::::IncorrectRound); ensure!( - project_details.hrmp_channel_status == + migration_info.hrmp_channel_status == HRMPChannelStatus { project_to_polimec: ChannelStatus::Open, polimec_to_project: ChannelStatus::Open }, Error::::ChannelNotOpen ); - if project_details.migration_readiness_check.is_none() { + if migration_info.migration_readiness_check.is_none() { ensure!(caller.clone() == T::PalletId::get().into_account_truncating(), Error::::NotAllowed); } else if matches!( - project_details.migration_readiness_check, - Some(MigrationReadinessCheck { + migration_info.migration_readiness_check, + Some(PalletMigrationReadinessCheck { holding_check: (_, CheckOutcome::Failed), pallet_check: (_, CheckOutcome::Failed), .. @@ -203,7 +281,7 @@ impl Pallet { Here, ); - project_details.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), }); @@ -253,13 +331,13 @@ impl Pallet { ) -> DispatchResult { use xcm::v3::prelude::*; // TODO: check if this is too low performance. Maybe we want a new map of query_id -> project_id - let (project_id, mut project_details, mut migration_check) = ProjectsDetails::::iter() + let (project_id, mut migration_info, mut project_details) = ProjectsDetails::::iter() .find_map(|(project_id, details)| { - if let Some(check @ MigrationReadinessCheck { holding_check, pallet_check }) = - details.migration_readiness_check - { - if holding_check.0 == query_id || pallet_check.0 == query_id { - return Some((project_id, details, check)); + if let Some(MigrationType::Pallet(ref info)) = details.migration_type { + if let Some(check) = info.migration_readiness_check { + if check.holding_check.0 == query_id || check.pallet_check.0 == query_id { + return Some((project_id, info.clone(), details)); + } } } None @@ -271,16 +349,21 @@ impl Pallet { } else { return Err(Error::::WrongParaId.into()); }; + ensure!(migration_info.parachain_id == para_id, Error::::WrongParaId); let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let contribution_tokens_sold = project_metadata.total_allocation_size.saturating_sub(project_details.remaining_contribution_tokens); - ensure!(project_details.parachain_id == Some(para_id), Error::::WrongParaId); - match (response.clone(), migration_check) { + match (response.clone(), &mut migration_info.migration_readiness_check) { ( Response::Assets(assets), - MigrationReadinessCheck { holding_check: (_, CheckOutcome::AwaitingResponse), .. }, + &mut Some( + ref mut check @ PalletMigrationReadinessCheck { + holding_check: (_, CheckOutcome::AwaitingResponse), + .. + }, + ), ) => { let ct_sold_as_u128: u128 = contribution_tokens_sold.try_into().map_err(|_| Error::::BadMath)?; let assets: Vec = assets.into_inner(); @@ -290,7 +373,7 @@ impl Pallet { id: Concrete(MultiLocation { parents: 1, interior: X1(Parachain(pid)) }), fun: Fungible(amount), } if amount >= ct_sold_as_u128 && pid == u32::from(para_id) => { - migration_check.holding_check.1 = CheckOutcome::Passed(None); + check.holding_check.1 = CheckOutcome::Passed(None); Self::deposit_event(Event::::MigrationCheckResponseAccepted { project_id, query_id, @@ -298,7 +381,7 @@ impl Pallet { }); }, _ => { - migration_check.holding_check.1 = CheckOutcome::Failed; + check.holding_check.1 = CheckOutcome::Failed; Self::deposit_event(Event::::MigrationCheckResponseRejected { project_id, query_id, @@ -310,7 +393,12 @@ impl Pallet { ( Response::PalletsInfo(pallets_info), - 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)?; @@ -319,17 +407,17 @@ impl Pallet { }; let u8_index: u8 = (*index).try_into().map_err(|_| Error::::NotAllowed)?; if pallets_info.len() == 1 && module_name == &expected_module_name { - migration_check.pallet_check.1 = CheckOutcome::Passed(Some(u8_index)); + check.pallet_check.1 = CheckOutcome::Passed(Some(u8_index)); Self::deposit_event(Event::::MigrationCheckResponseAccepted { project_id, query_id, response }); } else { - migration_check.pallet_check.1 = CheckOutcome::Failed; + check.pallet_check.1 = CheckOutcome::Failed; Self::deposit_event(Event::::MigrationCheckResponseRejected { project_id, query_id, response }); } }, _ => return Err(Error::::NotAllowed.into()), }; - project_details.migration_readiness_check = Some(migration_check); + project_details.migration_type = Some(MigrationType::Pallet(migration_info)); ProjectsDetails::::insert(project_id, project_details); Ok(()) } @@ -338,25 +426,29 @@ impl Pallet { /// This entails transferring the funds from the Polimec sovereign account to the participant account, and applying /// a vesting schedule if necessary. #[transactional] - pub fn do_migrate_one_participant(project_id: ProjectId, participant: AccountIdOf) -> DispatchResult { + pub fn do_send_pallet_migration_for(project_id: ProjectId, participant: AccountIdOf) -> DispatchResult { // * Get variables * let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - let migration_readiness_check = project_details.migration_readiness_check.ok_or(Error::::ChannelNotReady)?; - let project_para_id = project_details.parachain_id.ok_or(Error::::ImpossibleState)?; + let migration_info = match project_details.migration_type { + Some(MigrationType::Pallet(info)) => info, + _ => return Err(Error::::NotAllowed.into()), + }; + let migration_readiness_check = migration_info.migration_readiness_check.ok_or(Error::::ChannelNotReady)?; + let project_para_id = migration_info.parachain_id; let now = >::block_number(); ensure!( Self::user_has_no_participations(project_id, participant.clone()), Error::::ParticipationsNotSettled ); let (_, migrations) = - UserMigrations::::get(project_id, participant.clone()).ok_or(Error::::NoMigrationsFound)?; + UserMigrations::::get((project_id, participant.clone())).ok_or(Error::::NoMigrationsFound)?; // * Validity Checks * ensure!(migration_readiness_check.is_ready(), Error::::ChannelNotReady); 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_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); @@ -383,14 +475,22 @@ 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_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)?; let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + let migration_info = match project_details.migration_type { + Some(MigrationType::Pallet(info)) => info, + _ => return Err(Error::::NotAllowed.into()), + }; ensure!( - matches!(location, MultiLocation { parents: 1, interior: X1(Parachain(para_id))} if Some(ParaId::from(para_id)) == project_details.parachain_id), + matches!(location, MultiLocation { parents: 1, interior: X1(Parachain(para_id))} if ParaId::from(para_id) == migration_info.parachain_id), Error::::WrongParaId ); @@ -406,7 +506,32 @@ impl Pallet { }, _ => return Err(Error::::NotAllowed.into()), }; + Self::deposit_event(Event::::MigrationStatusUpdated { project_id, account: participant, status }); Ok(()) } } + +// Common migration functions +impl Pallet { + #[transactional] + pub fn do_mark_project_ct_migration_as_finished(project_id: ProjectId) -> DispatchResult { + // * Get variables * + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + + // * Validity checks * + ensure!(project_details.status == ProjectStatus::CTMigrationStarted, Error::::IncorrectRound); + + let unmigrated_participants = UnmigratedCounter::::get(project_id); + ensure!(unmigrated_participants == 0, Error::::MigrationsStillPending); + + // * Update storage * + project_details.status = ProjectStatus::CTMigrationFinished; + ProjectsDetails::::insert(project_id, project_details); + + // * Emit events * + Self::deposit_event(Event::CTMigrationFinished { project_id }); + + Ok(()) + } +} diff --git a/pallets/funding/src/functions/misc.rs b/pallets/funding/src/functions/misc.rs index aae5b1174..19f33866c 100644 --- a/pallets/funding/src/functions/misc.rs +++ b/pallets/funding/src/functions/misc.rs @@ -598,22 +598,34 @@ impl Pallet { ]) } - pub(crate) fn change_migration_status( + pub fn change_migration_status( project_id: ProjectId, user: T::AccountId, status: MigrationStatus, ) -> DispatchResult { + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; let (current_status, migrations) = - UserMigrations::::get(project_id, user.clone()).ok_or(Error::::NoMigrationsFound)?; + UserMigrations::::get((project_id, user.clone())).ok_or(Error::::NoMigrationsFound)?; + let status = match status { MigrationStatus::Sent(_) if matches!(current_status, MigrationStatus::NotStarted | MigrationStatus::Failed) => status, - MigrationStatus::Confirmed if matches!(current_status, MigrationStatus::Sent(_)) => status, + MigrationStatus::Confirmed + if matches!(project_details.migration_type, Some(MigrationType::Offchain)) || + (matches!(project_details.migration_type, Some(MigrationType::Pallet(_))) && + matches!(current_status, MigrationStatus::Sent(_))) => + { + UnmigratedCounter::::mutate(project_id, |counter| *counter = counter.saturating_sub(1)); + status + }, MigrationStatus::Failed if matches!(current_status, MigrationStatus::Sent(_)) => status, + _ => return Err(Error::::NotAllowed.into()), }; - UserMigrations::::insert(project_id, user, (status, migrations)); + UserMigrations::::insert((project_id, user), (status, migrations)); + ProjectsDetails::::insert(project_id, project_details); + Ok(()) } } 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 1ec1182e2..377ca31ab 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -314,12 +314,7 @@ impl< }, usd_bid_on_oversubscription: None, funding_end_block: None, - parachain_id: None, - migration_readiness_check: None, - hrmp_channel_status: HRMPChannelStatus { - project_to_polimec: crate::ChannelStatus::Closed, - polimec_to_project: crate::ChannelStatus::Closed, - }, + migration_type: None, }; assert_eq!(metadata, expected_metadata); assert_eq!(details, expected_details); @@ -722,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), - _ => panic!("Project should be in FundingSuccessful or FundingFailed status"), + 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 SettlementStarted 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> { @@ -828,7 +829,7 @@ impl< percentage: u64, ) { let details = self.get_project_details(project_id); - assert!(matches!(details.status, ProjectStatus::FundingSuccessful | ProjectStatus::FundingFailed)); + assert!(matches!(details.status, ProjectStatus::SettlementFinished(_))); for evaluation in evaluations { let reward_info = self @@ -912,7 +913,7 @@ impl< participation_type: ParticipationType, should_exist: bool, ) { - match (should_exist, self.execute(|| UserMigrations::::get(project_id, account.clone()))) { + match (should_exist, self.execute(|| UserMigrations::::get((project_id, account.clone())))) { // User has migrations, so we need to check if any matches our criteria (_, Some((_, migrations))) => { let maybe_migration = migrations.into_iter().find(|migration| { diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index e258aa1d5..cb2f8620a 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -538,15 +538,17 @@ pub mod pallet { StorageMap<_, Blake2_128Concat, Did, BoundedVec, ValueQuery>; #[pallet::storage] - pub type UserMigrations = StorageDoubleMap< + pub type UserMigrations = StorageNMap< _, - Blake2_128Concat, - ProjectId, - Blake2_128Concat, - T::AccountId, + (NMapKey, NMapKey>), (MigrationStatus, BoundedVec>), >; + /// Counts how many participants have not yet migrated their CTs. Counter goes up on each settlement, and goes + /// down on each migration. Saves us a whole read over the full migration storage for transitioning to `ProjectStatus::CTMigrationFinished` + #[pallet::storage] + pub type UnmigratedCounter = StorageMap<_, Blake2_128Concat, ProjectId, u32, ValueQuery>; + pub struct MaxParticipationsPerUser(PhantomData); impl Get for MaxParticipationsPerUser { fn get() -> u32 { @@ -651,7 +653,7 @@ pub mod pallet { id: u32, ct_amount: BalanceOf, }, - ProjectParaIdSet { + PalletMigrationStarted { project_id: ProjectId, para_id: ParaId, }, @@ -685,6 +687,10 @@ pub mod pallet { account: AccountIdOf, status: MigrationStatus, }, + + CTMigrationFinished { + project_id: ProjectId, + }, } #[pallet::error] @@ -810,12 +816,18 @@ pub mod pallet { WrongParaId, /// Migration channel is not ready for migrations. ChannelNotReady, - /// Settlement for this project has not yet started. - SettlementNotStarted, + /// Settlement for this project/outcome has not yet started. + FundingSuccessSettlementNotStarted, + /// Settlement for this project/outcome has not yet started. + FundingFailedSettlementNotStarted, /// Wanted to settle as successful when it failed, or vice versa. 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, + /// Tried to mark a project's CT migration as finished but there are still migrations to be confirmed + MigrationsStillPending, } #[pallet::call] @@ -1098,7 +1110,7 @@ pub mod pallet { #[pallet::call_index(22)] #[pallet::weight(Weight::from_parts(1000, 0))] - pub fn set_para_id_for_project( + pub fn configure_receiver_pallet_migration( origin: OriginFor, jwt: UntrustedToken, project_id: ProjectId, @@ -1108,7 +1120,7 @@ pub mod pallet { T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; ensure!(investor_type == InvestorType::Institutional, Error::::WrongInvestorType); - Self::do_set_para_id_for_project(&account, project_id, para_id) + Self::do_start_pallet_migration(&account, project_id, para_id) } #[pallet::call_index(23)] @@ -1139,21 +1151,37 @@ pub mod pallet { #[pallet::call_index(26)] #[pallet::weight(Weight::from_parts(1000, 0))] - pub fn migrate_one_participant( + pub fn send_pallet_migration_for( origin: OriginFor, project_id: ProjectId, participant: AccountIdOf, ) -> DispatchResult { let _caller = ensure_signed(origin)?; - Self::do_migrate_one_participant(project_id, participant) + Self::do_send_pallet_migration_for(project_id, participant) } #[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_pallet_migrations( + 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_pallet_migrations(location, query_id, response) + } + + #[pallet::call_index(38)] + #[pallet::weight(Weight::from_parts(1000, 0))] + pub fn confirm_offchain_migration( + origin: OriginFor, + project_id: ProjectId, + participant: AccountIdOf, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + + Self::do_confirm_offchain_migration(project_id, caller, participant) } #[pallet::call_index(28)] diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 5c0a08849..92005f843 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -138,6 +138,19 @@ impl ExecuteXcm for MockXcmExecutor { Ok(()) } } +pub struct DummyXcmSender; +impl SendXcm for DummyXcmSender { + type Ticket = (); + + fn validate(_: &mut Option, _: &mut Option>) -> SendResult { + Ok(((), MultiAssets::new())) + } + + /// Actually carry out the delivery operation for a previously validated message sending. + fn deliver(_ticket: Self::Ticket) -> Result { + Ok([0u8; 32]) + } +} impl pallet_xcm::Config for TestRuntime { type AdminOrigin = EnsureRoot; @@ -164,7 +177,7 @@ impl pallet_xcm::Config for TestRuntime { // Needs to be `Everything` for local testing. type XcmExecutor = MockXcmExecutor; type XcmReserveTransferFilter = Everything; - type XcmRouter = (); + type XcmRouter = DummyXcmSender; type XcmTeleportFilter = Everything; const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; diff --git a/pallets/funding/src/storage_migrations.rs b/pallets/funding/src/storage_migrations.rs index 3ccf42c5f..cf46d54c1 100644 --- a/pallets/funding/src/storage_migrations.rs +++ b/pallets/funding/src/storage_migrations.rs @@ -4,278 +4,3 @@ use frame_support::traits::StorageVersion; /// The current storage version pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); pub const LOG: &str = "runtime::funding::migration"; - -pub mod v2 { - use crate::{AccountIdOf, BalanceOf, Config, ProjectsMetadata}; - use frame_support::{ - pallet_prelude::{Decode, Encode, MaxEncodedLen, RuntimeDebug, TypeInfo}, - traits::{Get, OnRuntimeUpgrade}, - BoundedVec, - }; - use polimec_common::USD_DECIMALS; - use sp_arithmetic::{FixedPointNumber, Percent}; - use sp_core::ConstU32; - use sp_std::marker::PhantomData; - - #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] - pub struct OldTicketSize { - pub usd_minimum_per_participation: Option, - pub usd_maximum_per_did: Option, - } - - #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] - pub struct OldBiddingTicketSizes { - pub professional: OldTicketSize, - pub institutional: OldTicketSize, - pub phantom: PhantomData<(Price, Balance)>, - } - - #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] - pub struct OldContributingTicketSizes { - pub retail: OldTicketSize, - pub professional: OldTicketSize, - pub institutional: OldTicketSize, - pub phantom: PhantomData<(Price, Balance)>, - } - - type OldProjectMetadataOf = OldProjectMetadata< - BoundedVec>, - BalanceOf, - crate::PriceOf, - AccountIdOf, - polimec_common::credentials::Cid, - >; - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] - pub struct OldProjectMetadata { - /// Token Metadata - pub token_information: crate::CurrencyMetadata, - /// Mainnet Token Max Supply - pub mainnet_token_max_supply: Balance, - /// Total allocation of Contribution Tokens available for the Funding Round. - pub total_allocation_size: Balance, - /// Percentage of the total allocation of Contribution Tokens available for the Auction Round - pub auction_round_allocation_percentage: Percent, - /// The minimum price per token in USD, decimal-aware. See [`calculate_decimals_aware_price()`](crate::traits::ProvideAssetPrice::calculate_decimals_aware_price) for more information. - pub minimum_price: Price, - /// Maximum and minimum ticket sizes for auction round - pub bidding_ticket_sizes: OldBiddingTicketSizes, - /// Maximum and minimum ticket sizes for community/remainder rounds - pub contributing_ticket_sizes: OldContributingTicketSizes, - /// Participation currencies (e.g stablecoin, DOT, KSM) - pub participation_currencies: - BoundedVec>, - pub funding_destination_account: AccountId, - /// Additional metadata - pub policy_ipfs_cid: Option, - } - - pub struct UncheckedMigrationToV2(PhantomData); - impl OnRuntimeUpgrade for UncheckedMigrationToV2 { - fn on_runtime_upgrade() -> frame_support::weights::Weight { - let mut items = 0; - let mut translate = |_key, item: OldProjectMetadataOf| -> Option> { - items += 1; - let usd_unit = sp_arithmetic::traits::checked_pow(BalanceOf::::from(10u64), USD_DECIMALS as usize)?; - Some(crate::ProjectMetadataOf:: { - token_information: item.token_information, - mainnet_token_max_supply: item.mainnet_token_max_supply, - total_allocation_size: item.total_allocation_size, - auction_round_allocation_percentage: item.auction_round_allocation_percentage, - minimum_price: item.minimum_price, - bidding_ticket_sizes: crate::BiddingTicketSizes { - professional: crate::TicketSize { - usd_minimum_per_participation: item - .bidding_ticket_sizes - .professional - .usd_minimum_per_participation - .unwrap_or_else(|| usd_unit), - usd_maximum_per_did: item.bidding_ticket_sizes.professional.usd_maximum_per_did, - }, - institutional: crate::TicketSize { - usd_minimum_per_participation: item - .bidding_ticket_sizes - .institutional - .usd_minimum_per_participation - .unwrap_or_else(|| usd_unit), - usd_maximum_per_did: item.bidding_ticket_sizes.institutional.usd_maximum_per_did, - }, - phantom: Default::default(), - }, - contributing_ticket_sizes: crate::ContributingTicketSizes { - retail: crate::TicketSize { - usd_minimum_per_participation: item - .contributing_ticket_sizes - .retail - .usd_minimum_per_participation - .unwrap_or_else(|| usd_unit), - usd_maximum_per_did: item.contributing_ticket_sizes.retail.usd_maximum_per_did, - }, - professional: crate::TicketSize { - usd_minimum_per_participation: item - .contributing_ticket_sizes - .professional - .usd_minimum_per_participation - .unwrap_or_else(|| usd_unit), - usd_maximum_per_did: item.contributing_ticket_sizes.professional.usd_maximum_per_did, - }, - institutional: crate::TicketSize { - usd_minimum_per_participation: item - .contributing_ticket_sizes - .institutional - .usd_minimum_per_participation - .unwrap_or_else(|| usd_unit), - usd_maximum_per_did: item.contributing_ticket_sizes.institutional.usd_maximum_per_did, - }, - phantom: Default::default(), - }, - participation_currencies: item.participation_currencies, - funding_destination_account: item.funding_destination_account, - policy_ipfs_cid: item.policy_ipfs_cid, - }) - }; - - ProjectsMetadata::::translate(|key, object: OldProjectMetadataOf| translate(key, object)); - - T::DbWeight::get().reads_writes(items, items) - } - } - - pub type MigrationToV2 = frame_support::migrations::VersionedMigration< - 1, - 2, - UncheckedMigrationToV2, - crate::Pallet, - ::DbWeight, - >; -} - -pub mod v3 { - use crate::{ - AccountIdOf, BalanceOf, Config, EvaluationRoundInfoOf, HRMPChannelStatus, MigrationReadinessCheck, - PhaseTransitionPoints, PriceOf, ProjectDetailsOf, ProjectStatus, - }; - use frame_support::{ - pallet_prelude::Get, - traits::{tokens::Balance as BalanceT, OnRuntimeUpgrade}, - }; - use frame_system::pallet_prelude::BlockNumberFor; - use polimec_common::credentials::Did; - use polkadot_parachain_primitives::primitives::Id as ParaId; - use scale_info::TypeInfo; - use sp_arithmetic::FixedPointNumber; - use sp_core::{Decode, Encode, MaxEncodedLen, RuntimeDebug}; - use sp_std::marker::PhantomData; - - #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] - pub enum OldProjectStatus { - #[default] - Application, - EvaluationRound, - AuctionInitializePeriod, - AuctionOpening, - AuctionClosing, - CommunityRound, - RemainderRound, - FundingFailed, - AwaitingProjectDecision, - FundingSuccessful, - ReadyToStartMigration, - MigrationCompleted, - } - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - pub struct OldProjectDetails< - AccountId, - Did, - BlockNumber, - Price: FixedPointNumber, - Balance: BalanceT, - EvaluationRoundInfo, - > { - pub issuer_account: AccountId, - pub issuer_did: Did, - /// Whether the project is frozen, so no `metadata` changes are allowed. - pub is_frozen: bool, - /// The price in USD per token decided after the Auction Round - pub weighted_average_price: Option, - /// The current status of the project - pub status: OldProjectStatus, - /// When the different project phases start and end - pub phase_transition_points: PhaseTransitionPoints, - /// Fundraising target amount in USD (6 decimals) - pub fundraising_target_usd: Balance, - /// The amount of Contribution Tokens that have not yet been sold - pub remaining_contribution_tokens: Balance, - /// Funding reached amount in USD (6 decimals) - pub funding_amount_reached_usd: Balance, - /// Information about the total amount bonded, and the outcome in regards to reward/slash/nothing - pub evaluation_round_info: EvaluationRoundInfo, - /// When the Funding Round ends - pub funding_end_block: Option, - /// ParaId of project - pub parachain_id: Option, - /// Migration readiness check - pub migration_readiness_check: Option, - /// HRMP Channel status - pub hrmp_channel_status: HRMPChannelStatus, - } - type OldProjectDetailsOf = - OldProjectDetails, Did, BlockNumberFor, PriceOf, BalanceOf, EvaluationRoundInfoOf>; - - pub struct UncheckedMigrationToV3(PhantomData); - impl OnRuntimeUpgrade for UncheckedMigrationToV3 { - fn on_runtime_upgrade() -> frame_support::weights::Weight { - let mut items = 0; - let mut translate = |_key, item: OldProjectDetailsOf| -> Option> { - items += 1; - let new_status = match item.status { - OldProjectStatus::Application => ProjectStatus::Application, - OldProjectStatus::EvaluationRound => ProjectStatus::EvaluationRound, - OldProjectStatus::AuctionInitializePeriod => ProjectStatus::AuctionInitializePeriod, - OldProjectStatus::AuctionOpening => ProjectStatus::AuctionOpening, - OldProjectStatus::AuctionClosing => ProjectStatus::AuctionClosing, - OldProjectStatus::CommunityRound => ProjectStatus::CommunityRound, - OldProjectStatus::RemainderRound => ProjectStatus::RemainderRound, - OldProjectStatus::FundingFailed => ProjectStatus::FundingFailed, - OldProjectStatus::AwaitingProjectDecision => ProjectStatus::AwaitingProjectDecision, - OldProjectStatus::FundingSuccessful => ProjectStatus::FundingSuccessful, - OldProjectStatus::ReadyToStartMigration => ProjectStatus::ReadyToStartMigration, - OldProjectStatus::MigrationCompleted => ProjectStatus::MigrationCompleted, - }; - Some(ProjectDetailsOf:: { - issuer_account: item.issuer_account, - issuer_did: item.issuer_did, - is_frozen: item.is_frozen, - weighted_average_price: item.weighted_average_price, - status: new_status, - phase_transition_points: item.phase_transition_points, - fundraising_target_usd: item.fundraising_target_usd, - remaining_contribution_tokens: item.remaining_contribution_tokens, - funding_amount_reached_usd: item.funding_amount_reached_usd, - evaluation_round_info: item.evaluation_round_info, - usd_bid_on_oversubscription: None, - funding_end_block: item.funding_end_block, - parachain_id: item.parachain_id, - migration_readiness_check: item.migration_readiness_check, - hrmp_channel_status: item.hrmp_channel_status, - }) - }; - - crate::ProjectsDetails::::translate(|key, object: OldProjectDetailsOf| translate(key, object)); - - T::DbWeight::get().reads_writes(items, items) - } - } - - pub type MigrationToV3 = frame_support::migrations::VersionedMigration< - 2, - 3, - UncheckedMigrationToV3, - crate::Pallet, - ::DbWeight, - >; -} diff --git a/pallets/funding/src/tests/1_application.rs b/pallets/funding/src/tests/1_application.rs index 45dd51722..cd4486ba5 100644 --- a/pallets/funding/src/tests/1_application.rs +++ b/pallets/funding/src/tests/1_application.rs @@ -162,7 +162,10 @@ mod create_project_extrinsic { ); }); inst.advance_time(::EvaluationDuration::get() + 1).unwrap(); - assert_eq!(inst.get_project_details(0).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(0).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); inst.execute(|| { assert_ok!(Pallet::::create_project( RuntimeOrigin::signed(ISSUER_1), diff --git a/pallets/funding/src/tests/2_evaluation.rs b/pallets/funding/src/tests/2_evaluation.rs index ff0b09923..34d6021b5 100644 --- a/pallets/funding/src/tests/2_evaluation.rs +++ b/pallets/funding/src/tests/2_evaluation.rs @@ -71,7 +71,7 @@ mod round_flow { let now = inst.current_block(); inst.advance_time(update_block - now + 1).unwrap(); let project_status = inst.get_project_details(project_id).status; - assert_eq!(project_status, ProjectStatus::FundingFailed); + assert_eq!(project_status, ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed)); } #[test] @@ -242,9 +242,6 @@ mod round_flow { assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); - // Cannot settle before the settlement starts - inst.settle_project(project_id).expect_err("Settlement should not be possible yet"); - let settlement_block = inst.get_update_block(project_id, &UpdateType::StartSettlement).unwrap(); inst.jump_to_block(settlement_block); @@ -329,12 +326,7 @@ mod start_evaluation_extrinsic { }, usd_bid_on_oversubscription: None, funding_end_block: None, - parachain_id: None, - migration_readiness_check: None, - hrmp_channel_status: HRMPChannelStatus { - project_to_polimec: ChannelStatus::Closed, - polimec_to_project: ChannelStatus::Closed, - }, + migration_type: None, }; assert_ok!(inst.execute(|| PolimecFunding::start_evaluation( RuntimeOrigin::signed(issuer), diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index 18c7518f1..0cb2bba20 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -825,7 +825,10 @@ mod start_auction_extrinsic { Error::::TransitionPointNotSet ); }); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); } #[test] diff --git a/pallets/funding/src/tests/6_funding_end.rs b/pallets/funding/src/tests/6_funding_end.rs index 5aa2ee480..f76d94ba5 100644 --- a/pallets/funding/src/tests/6_funding_end.rs +++ b/pallets/funding/src/tests/6_funding_end.rs @@ -9,7 +9,10 @@ mod round_flow { #[test] fn evaluator_slash_is_decided() { let (mut inst, project_id) = create_project_with_funding_percentage(20, None, true); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); assert_eq!( inst.get_project_details(project_id).evaluation_round_info.evaluators_outcome, EvaluatorsOutcome::Slashed @@ -20,7 +23,10 @@ mod round_flow { fn evaluator_unchanged_is_decided() { let (mut inst, project_id) = create_project_with_funding_percentage(80, Some(FundingOutcomeDecision::AcceptFunding), true); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingSuccessful); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) + ); assert_eq!( inst.get_project_details(project_id).evaluation_round_info.evaluators_outcome, EvaluatorsOutcome::Unchanged @@ -32,7 +38,10 @@ mod round_flow { let (mut inst, project_id) = create_project_with_funding_percentage(95, None, true); let project_details = inst.get_project_details(project_id); let project_metadata = inst.get_project_metadata(project_id); - assert_eq!(project_details.status, ProjectStatus::FundingSuccessful); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) + ); // We want to test rewards over the 3 brackets, which means > 5MM USD funded const USD_REACHED: u128 = 9_500_000 * USD_UNIT; diff --git a/pallets/funding/src/tests/7_settlement.rs b/pallets/funding/src/tests/7_settlement.rs index 9c4bdc498..ab957065f 100644 --- a/pallets/funding/src/tests/7_settlement.rs +++ b/pallets/funding/src/tests/7_settlement.rs @@ -266,7 +266,7 @@ mod settle_successful_evaluation_extrinsic { evaluator, first_evaluation.id ), - Error::::WrongSettlementOutcome + Error::::FundingSuccessSettlementNotStarted ); }); } @@ -287,7 +287,7 @@ mod settle_successful_evaluation_extrinsic { evaluator, first_evaluation.id ), - Error::::SettlementNotStarted + Error::::FundingSuccessSettlementNotStarted ); }); } @@ -608,7 +608,7 @@ mod settle_successful_bid_extrinsic { bidder, first_bid.id ), - Error::::WrongSettlementOutcome + Error::::FundingSuccessSettlementNotStarted ); }); } @@ -628,7 +628,7 @@ mod settle_successful_bid_extrinsic { bidder, first_bid.id ), - Error::::SettlementNotStarted + Error::::FundingSuccessSettlementNotStarted ); }); } @@ -897,7 +897,7 @@ mod settle_successful_contribution_extrinsic { contributor, first_contribution.id ), - Error::::WrongSettlementOutcome + Error::::FundingSuccessSettlementNotStarted ); }); } @@ -916,7 +916,7 @@ mod settle_successful_contribution_extrinsic { contributor, first_contribution.id ), - Error::::SettlementNotStarted + Error::::FundingSuccessSettlementNotStarted ); }); } @@ -1036,7 +1036,7 @@ mod settle_failed_evaluation_extrinsic { evaluator, first_evaluation.id ), - Error::::WrongSettlementOutcome + Error::::FundingFailedSettlementNotStarted ); }); } @@ -1057,7 +1057,7 @@ mod settle_failed_evaluation_extrinsic { evaluator, first_evaluation.id ), - Error::::SettlementNotStarted + Error::::FundingFailedSettlementNotStarted ); }); } @@ -1239,7 +1239,7 @@ mod settle_failed_bid_extrinsic { bidder, first_bid.id ), - Error::::WrongSettlementOutcome + Error::::FundingFailedSettlementNotStarted ); }); } @@ -1259,7 +1259,7 @@ mod settle_failed_bid_extrinsic { bidder, first_bid.id ), - Error::::SettlementNotStarted + Error::::FundingFailedSettlementNotStarted ); }); } @@ -1506,7 +1506,7 @@ mod settle_failed_contribution_extrinsic { contributor, first_contribution.id ), - Error::::WrongSettlementOutcome + Error::::FundingFailedSettlementNotStarted ); }); } @@ -1526,7 +1526,7 @@ mod settle_failed_contribution_extrinsic { contributor, first_contribution.id ), - Error::::SettlementNotStarted + Error::::FundingFailedSettlementNotStarted ); }); } diff --git a/pallets/funding/src/tests/8_ct_migration.rs b/pallets/funding/src/tests/8_ct_migration.rs index d26601554..9c15284d9 100644 --- a/pallets/funding/src/tests/8_ct_migration.rs +++ b/pallets/funding/src/tests/8_ct_migration.rs @@ -1,57 +1,285 @@ use super::*; -use frame_support::assert_err; - -#[test] -fn para_id_for_project_can_be_set_by_issuer() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_id = inst.create_finished_project( - default_project_metadata(ISSUER_1), - ISSUER_1, - default_evaluations(), - default_bids(), - default_community_buys(), - default_remainder_buys(), - ); - - inst.advance_time(::SuccessToSettlementTime::get() + 20u64).unwrap(); - inst.execute(|| { - assert_ok!(crate::Pallet::::do_set_para_id_for_project( - &ISSUER_1, - project_id, - ParaId::from(2006u32), - )); - }); - let project_details = inst.get_project_details(project_id); - assert_eq!(project_details.parachain_id, Some(ParaId::from(2006u32))); +use frame_support::{assert_err, traits::fungibles::Inspect}; +use sp_runtime::bounded_vec; +use xcm::latest::MaxPalletNameLen; + +mod pallet_migration { + use super::*; + + #[test] + fn start_pallet_migration() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER_1), + ISSUER_1, + default_evaluations(), + default_bids(), + default_community_buys(), + default_remainder_buys(), + ); + inst.advance_time(::SuccessToSettlementTime::get()).unwrap(); + inst.settle_project(project_id).unwrap(); + + inst.execute(|| { + assert_err!( + crate::Pallet::::do_start_pallet_migration( + &EVALUATOR_1, + project_id, + ParaId::from(2006u32), + ), + Error::::NotIssuer + ); + assert_err!( + crate::Pallet::::do_start_pallet_migration(&BIDDER_1, project_id, ParaId::from(2006u32),), + Error::::NotIssuer + ); + assert_err!( + crate::Pallet::::do_start_pallet_migration(&BUYER_1, project_id, ParaId::from(2006u32),), + Error::::NotIssuer + ); + assert_ok!(crate::Pallet::::do_start_pallet_migration( + &ISSUER_1, + project_id, + ParaId::from(2006u32).into(), + )); + }); + + let project_details = inst.get_project_details(project_id); + assert_eq!( + project_details.migration_type, + Some(MigrationType::Pallet(PalletMigrationInfo { + parachain_id: 2006.into(), + hrmp_channel_status: HRMPChannelStatus { + project_to_polimec: ChannelStatus::Closed, + polimec_to_project: ChannelStatus::Closed + }, + migration_readiness_check: None, + })) + ); + assert_eq!(project_details.status, ProjectStatus::CTMigrationStarted); + assert_eq!(inst.execute(|| UnmigratedCounter::::get(project_id)), 10); + } + + fn create_pallet_migration_project(mut inst: MockInstantiator) -> (ProjectId, MockInstantiator) { + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER_1), + ISSUER_1, + default_evaluations(), + default_bids(), + default_community_buys(), + default_remainder_buys(), + ); + inst.advance_time(::SuccessToSettlementTime::get()).unwrap(); + inst.settle_project(project_id).unwrap(); + inst.execute(|| { + assert_ok!(crate::Pallet::::do_start_pallet_migration( + &ISSUER_1, + project_id, + ParaId::from(6969u32) + )); + }); + (project_id, inst) + } + + fn fake_hrmp_establishment() { + // Notification sent by the relay when the project starts a project->polimec channel + let open_channel_message = xcm::v3::opaque::Instruction::HrmpNewChannelOpenRequest { + sender: 6969, + max_message_size: 102_300, + max_capacity: 1000, + }; + // This makes Polimec send an acceptance + open channel (polimec->project) message back to the relay + assert_ok!(PolimecFunding::do_handle_channel_open_request(open_channel_message)); + + // Finally the relay notifies the channel polimec->project has been accepted by the project + let channel_accepted_message = xcm::v3::opaque::Instruction::HrmpChannelAccepted { recipient: 6969u32 }; + + // We set the hrmp flags as "Open" and start the receiver pallet check + assert_ok!(PolimecFunding::do_handle_channel_accepted(channel_accepted_message)); + } + + #[test] + fn automatic_hrmp_establishment() { + let inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let (project_id, mut inst) = create_pallet_migration_project(inst); + + inst.execute(|| fake_hrmp_establishment()); + + let project_details = inst.get_project_details(project_id); + assert_eq!( + project_details.migration_type, + Some(MigrationType::Pallet(PalletMigrationInfo { + parachain_id: 6969.into(), + hrmp_channel_status: HRMPChannelStatus { + project_to_polimec: ChannelStatus::Open, + polimec_to_project: ChannelStatus::Open + }, + migration_readiness_check: Some(PalletMigrationReadinessCheck { + holding_check: (0, CheckOutcome::AwaitingResponse), + pallet_check: (1, CheckOutcome::AwaitingResponse) + }), + })) + ); + } + + /// Check that the polimec sovereign account has the ct issuance on the project chain, and the receiver pallet is in + /// the runtime. + #[test] + fn pallet_readiness_check() { + let inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let (project_id, mut inst) = create_pallet_migration_project(inst); + inst.execute(|| fake_hrmp_establishment()); + + // At this point, we sent the pallet check xcm to the project chain, and we are awaiting a query response message. + // query id 0 is the CT balance of the Polimec SA + // query id 1 is the existence of the receiver pallet + + // We simulate the response from the project chain + let ct_issuance = + inst.execute(|| ::ContributionTokenCurrency::total_issuance(project_id)); + let ct_multiassets: MultiAssets = vec![MultiAsset { + id: Concrete(MultiLocation { parents: 1, interior: X1(Parachain(6969)) }), + fun: Fungibility::Fungible(ct_issuance), + }] + .into(); + + inst.execute(|| { + assert_ok!(PolimecFunding::do_migration_check_response( + MultiLocation::new(1u8, X1(Parachain(6969u32))), + 0u64, + Response::Assets(ct_multiassets), + )); + }); + + let module_name: BoundedVec = + BoundedVec::try_from("polimec_receiver".as_bytes().to_vec()).unwrap(); + let pallet_info = xcm::latest::PalletInfo { + // index is used for future `Transact` calls to the pallet for migrating a user + index: 69, + // Doesn't matter + name: module_name.clone(), + // Main check that the receiver pallet is there + module_name, + // These might be useful in the future, but not for now + major: 0, + minor: 0, + patch: 0, + }; + inst.execute(|| { + assert_ok!(PolimecFunding::do_migration_check_response( + MultiLocation::new(1u8, X1(Parachain(6969u32))), + 1u64, + Response::PalletsInfo(bounded_vec![pallet_info]), + )); + }); + + let project_details = inst.get_project_details(project_id); + if let MigrationType::Pallet(info) = project_details.migration_type.unwrap() { + assert_eq!(info.migration_readiness_check.unwrap().holding_check.1, CheckOutcome::Passed(None)); + assert_eq!(info.migration_readiness_check.unwrap().pallet_check.1, CheckOutcome::Passed(Some(69))); + } else { + panic!("Migration type is not Pallet") + } + } } -#[test] -fn para_id_for_project_cannot_be_set_by_anyone_but_issuer() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_id = inst.create_finished_project( - default_project_metadata(ISSUER_1), - ISSUER_1, - default_evaluations(), - default_bids(), - default_community_buys(), - default_remainder_buys(), - ); - inst.advance_time(::SuccessToSettlementTime::get() + 20u64).unwrap(); - - inst.execute(|| { - assert_err!( - crate::Pallet::::do_set_para_id_for_project(&EVALUATOR_1, project_id, ParaId::from(2006u32),), - Error::::NotIssuer +mod offchain_migration { + use super::*; + + #[test] + fn start_offchain_migration() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + // Create migrations for 2 projects, to check the `remaining_participants` is unaffected by other projects + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER_1), + ISSUER_1, + default_evaluations(), + default_bids(), + default_community_buys(), + default_remainder_buys(), ); - assert_err!( - crate::Pallet::::do_set_para_id_for_project(&BIDDER_1, project_id, ParaId::from(2006u32),), - Error::::NotIssuer + inst.advance_time(::SuccessToSettlementTime::get()).unwrap(); + inst.settle_project(project_id).unwrap(); + + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER_1), + ISSUER_1, + default_evaluations(), + default_bids(), + default_community_buys(), + default_remainder_buys(), ); - assert_err!( - crate::Pallet::::do_set_para_id_for_project(&BUYER_1, project_id, ParaId::from(2006u32),), - Error::::NotIssuer + inst.advance_time(::SuccessToSettlementTime::get()).unwrap(); + inst.settle_project(project_id).unwrap(); + + inst.execute(|| { + assert_err!( + crate::Pallet::::do_start_offchain_migration(project_id, EVALUATOR_1,), + Error::::NotIssuer + ); + + assert_ok!(crate::Pallet::::do_start_offchain_migration(project_id, ISSUER_1,)); + }); + + let project_details = inst.get_project_details(project_id); + assert_eq!(inst.execute(|| UnmigratedCounter::::get(project_id)), 10); + assert_eq!(project_details.status, ProjectStatus::CTMigrationStarted); + } + + fn create_offchain_migration_project(mut inst: MockInstantiator) -> (ProjectId, MockInstantiator) { + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER_1), + ISSUER_1, + default_evaluations(), + default_bids(), + default_community_buys(), + default_remainder_buys(), ); - }); - let project_details = inst.get_project_details(project_id); - assert_eq!(project_details.parachain_id, None); + inst.advance_time(::SuccessToSettlementTime::get()).unwrap(); + inst.settle_project(project_id).unwrap(); + inst.execute(|| { + assert_ok!(crate::Pallet::::do_start_offchain_migration(project_id, ISSUER_1,)); + }); + (project_id, inst) + } + + #[test] + fn confirm_offchain_migration() { + let inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let (project_id, mut inst) = create_offchain_migration_project(inst); + + let bidder_1_migrations = inst.execute(|| UserMigrations::::get((project_id, BIDDER_1))).unwrap(); + assert_eq!(bidder_1_migrations.0, MigrationStatus::NotStarted); + + inst.execute(|| { + assert_ok!(crate::Pallet::::do_confirm_offchain_migration(project_id, ISSUER_1, BIDDER_1)); + }); + + let bidder_1_migrations = inst.execute(|| UserMigrations::::get((project_id, BIDDER_1))).unwrap(); + assert_eq!(bidder_1_migrations.0, MigrationStatus::Confirmed); + } + + #[test] + fn mark_project_as_migration_finished() { + let inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let (project_id, mut inst) = create_offchain_migration_project(inst); + + let participants = inst.execute(|| UserMigrations::::iter_key_prefix((project_id,)).collect_vec()); + for participant in participants { + dbg!(inst.execute(|| UnmigratedCounter::::get(project_id))); + inst.execute(|| { + assert_ok!(crate::Pallet::::do_confirm_offchain_migration( + project_id, + ISSUER_1, + participant + )); + }); + } + + inst.execute(|| { + assert_ok!(crate::Pallet::::do_mark_project_ct_migration_as_finished(project_id)); + }); + } + + // Can't start if project is not settled } diff --git a/pallets/funding/src/tests/mod.rs b/pallets/funding/src/tests/mod.rs index 288923edd..90c2f5777 100644 --- a/pallets/funding/src/tests/mod.rs +++ b/pallets/funding/src/tests/mod.rs @@ -427,10 +427,16 @@ pub fn create_project_with_funding_percentage( _ => panic!("unexpected percentage"), }; if funding_sucessful { - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingSuccessful); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingSuccessful) + ); inst.test_ct_created_for(project_id); } else { - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + assert_eq!( + inst.get_project_details(project_id).status, + ProjectStatus::SettlementStarted(FundingOutcome::FundingFailed) + ); inst.test_ct_not_created_for(project_id); } } diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index 2fb3ebbf6..8554e5c3c 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -294,6 +294,22 @@ pub mod storage_types { Institutional(Bound), } + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub enum MigrationType { + Offchain, + Pallet(PalletMigrationInfo), + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub struct PalletMigrationInfo { + /// ParaId of project + pub parachain_id: ParaId, + /// HRMP Channel status + pub hrmp_channel_status: HRMPChannelStatus, + /// Migration readiness check + pub migration_readiness_check: Option, + } + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct ProjectDetails< AccountId, @@ -325,12 +341,7 @@ pub mod storage_types { pub usd_bid_on_oversubscription: Option, /// When the Funding Round ends pub funding_end_block: Option, - /// ParaId of project - pub parachain_id: Option, - /// Migration readiness check - pub migration_readiness_check: Option, - /// HRMP Channel status - pub hrmp_channel_status: HRMPChannelStatus, + pub migration_type: Option, } /// Tells on_initialize what to do with the project #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -665,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)] @@ -827,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(_))) diff --git a/pallets/linear-release/src/tests.rs b/pallets/linear-release/src/tests.rs index d5d81baa9..901e860b3 100644 --- a/pallets/linear-release/src/tests.rs +++ b/pallets/linear-release/src/tests.rs @@ -125,9 +125,9 @@ fn check_vesting_status_for_multi_schedule_account() { assert_eq!(Balances::balance_on_hold(&MockRuntimeHoldReason::Reason, &2), 20 * ED); assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched1, MockRuntimeHoldReason::Reason)); assert_eq!(Balances::balance_on_hold(&MockRuntimeHoldReason::Reason, &2), 29 * ED); // Why 29 and not 30? Because sched1 is already unlocking. - // Free balance is the one set in Genesis inside the Balances pallet - // + the one from the vested transfer. - // BUT NOT the one in sched0, since the vesting will start at block #10. + // Free balance is the one set in Genesis inside the Balances pallet + // + the one from the vested transfer. + // BUT NOT the one in sched0, since the vesting will start at block #10. let balance = Balances::balance(&2); assert_eq!(balance, ED * (2)); // The most recently added schedule exists. @@ -193,7 +193,7 @@ fn unvested_balance_should_not_transfer() { ExtBuilder::default().existential_deposit(10).build().execute_with(|| { let user1_free_balance = Balances::free_balance(1); assert_eq!(user1_free_balance, 50); // Account 1 has free balance - // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) assert_eq!(Vesting::vesting_balance(&1, MockRuntimeHoldReason::Reason), Some(5)); // Account 1 cannot send more than vested amount... assert_noop!(Balances::transfer_allow_death(Some(1).into(), 2, 56), TokenError::FundsUnavailable); }); @@ -205,13 +205,13 @@ fn vested_balance_should_transfer() { assert_eq!(System::block_number(), 1); let user1_free_balance = Balances::free_balance(1); assert_eq!(user1_free_balance, 50); // Account 1 has free balance - // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) assert_eq!(Vesting::vesting_balance(&1, MockRuntimeHoldReason::Reason), Some(5)); assert_noop!(Balances::transfer_allow_death(Some(1).into(), 2, 45), TokenError::Frozen); // Account 1 free balance - ED is < 45 assert_ok!(Vesting::vest(Some(1).into(), MockRuntimeHoldReason::Reason)); let user1_free_balance = Balances::free_balance(1); assert_eq!(user1_free_balance, 55); // Account 1 has free balance - // Account 1 has vested 1 unit at block 1 (plus 50 unvested) + // Account 1 has vested 1 unit at block 1 (plus 50 unvested) assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 45)); // After the vest it can now send the 45 UNIT }); } @@ -259,7 +259,7 @@ fn vested_balance_should_transfer_using_vest_other() { ExtBuilder::default().existential_deposit(10).build().execute_with(|| { let user1_free_balance = Balances::free_balance(1); assert_eq!(user1_free_balance, 50); // Account 1 has free balance - // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) assert_eq!(Vesting::vesting_balance(&1, MockRuntimeHoldReason::Reason), Some(5)); assert_ok!(Vesting::vest_other(Some(2).into(), 1, MockRuntimeHoldReason::Reason)); assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 55 - 10)); @@ -317,7 +317,7 @@ fn extra_balance_should_transfer() { // Account 2 has no units vested at block 1, but gained 100 assert_ok!(Balances::transfer_allow_death(Some(2).into(), 3, 100 - 10)); // Account 2 can send extra - // units gained + // units gained }); } @@ -327,7 +327,7 @@ fn liquid_funds_should_transfer_with_delayed_vesting() { let user12_free_balance = Balances::free_balance(12); assert_eq!(user12_free_balance, 1280); // Account 12 has free balance - // Account 12 has liquid funds + // Account 12 has liquid funds assert_eq!(Vesting::vesting_balance(&12, MockRuntimeHoldReason::Reason), Some(0)); // Account 12 has delayed vesting